/***********************************************************************************
|
*
|
* The MIT License (MIT)
|
*
|
* Copyright (c) 2013 Matthew Styles
|
*
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* this software and associated documentation files (the "Software"), to deal in
|
* the Software without restriction, including without limitation the rights to
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
* subject to the following conditions:
|
*
|
* The above copyright notice and this permission notice shall be included in all
|
* copies or substantial portions of the Software.
|
*
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
*
|
***********************************************************************************/
|
|
#import "KILabel.h"
|
|
NSString * const KILabelLinkTypeKey = @"linkType";
|
NSString * const KILabelRangeKey = @"range";
|
NSString * const KILabelLinkKey = @"link";
|
|
#pragma mark - Private Interface
|
|
@interface KILabel()
|
|
// Used to control layout of glyphs and rendering
|
@property (nonatomic, retain) NSLayoutManager *layoutManager;
|
|
// Specifies the space in which to render text
|
@property (nonatomic, retain) NSTextContainer *textContainer;
|
|
// Backing storage for text that is rendered by the layout manager
|
@property (nonatomic, retain) NSTextStorage *textStorage;
|
|
// Dictionary of detected links and their ranges in the text
|
@property (nonatomic, copy) NSArray *linkRanges;
|
|
// State used to trag if the user has dragged during a touch
|
@property (nonatomic, assign) BOOL isTouchMoved;
|
|
// During a touch, range of text that is displayed as selected
|
@property (nonatomic, assign) NSRange selectedRange;
|
|
@end
|
|
#pragma mark - Implementation
|
|
@implementation KILabel
|
{
|
NSMutableDictionary *_linkTypeAttributes;
|
}
|
|
#pragma mark - Construction
|
|
- (id)initWithFrame:(CGRect)frame
|
{
|
self = [super initWithFrame:frame];
|
if (self)
|
{
|
[self setupTextSystem];
|
}
|
|
return self;
|
}
|
|
- (id)initWithCoder:(NSCoder *)aDecoder
|
{
|
self = [super initWithCoder:aDecoder];
|
if (self)
|
{
|
[self setupTextSystem];
|
}
|
|
return self;
|
}
|
|
// Common initialisation. Must be done once during construction.
|
- (void)setupTextSystem
|
{
|
// Create a text container and set it up to match our label properties
|
_textContainer = [[NSTextContainer alloc] init];
|
_textContainer.lineFragmentPadding = 0;
|
_textContainer.maximumNumberOfLines = self.numberOfLines;
|
_textContainer.lineBreakMode = self.lineBreakMode;
|
_textContainer.size = self.frame.size;
|
|
// Create a layout manager for rendering
|
_layoutManager = [[NSLayoutManager alloc] init];
|
_layoutManager.delegate = self;
|
[_layoutManager addTextContainer:_textContainer];
|
|
// Attach the layou manager to the container and storage
|
[_textContainer setLayoutManager:_layoutManager];
|
|
// Make sure user interaction is enabled so we can accept touches
|
self.userInteractionEnabled = YES;
|
|
// Don't go via public setter as this will have undesired side effect
|
_automaticLinkDetectionEnabled = YES;
|
|
// All links are detectable by default
|
_linkDetectionTypes = KILinkTypeOptionAll;
|
|
// Link Type Attributes. Default is empty (no attributes).
|
_linkTypeAttributes = [NSMutableDictionary dictionary];
|
|
// Don't underline URL links by default.
|
_systemURLStyle = NO;
|
|
// By default we hilight the selected link during a touch to give feedback that we are
|
// responding to touch.
|
_selectedLinkBackgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
|
// Establish the text store with our current text
|
[self updateTextStoreWithText];
|
}
|
|
#pragma mark - Text and Style management
|
|
- (void)setAutomaticLinkDetectionEnabled:(BOOL)decorating
|
{
|
_automaticLinkDetectionEnabled = decorating;
|
|
// Make sure the text is updated properly
|
[self updateTextStoreWithText];
|
}
|
|
- (void)setLinkDetectionTypes:(KILinkTypeOption)linkDetectionTypes
|
{
|
_linkDetectionTypes = linkDetectionTypes;
|
|
// Make sure the text is updated properly
|
[self updateTextStoreWithText];
|
}
|
|
- (NSDictionary *)linkAtPoint:(CGPoint)location
|
{
|
// Do nothing if we have no text
|
if (_textStorage.string.length == 0)
|
{
|
return nil;
|
}
|
|
// Work out the offset of the text in the view
|
CGPoint textOffset = [self calcGlyphsPositionInView];
|
|
// Get the touch location and use text offset to convert to text cotainer coords
|
location.x -= textOffset.x;
|
location.y -= textOffset.y;
|
|
NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer];
|
|
// If the touch is in white space after the last glyph on the line we don't
|
// count it as a hit on the text
|
NSRange lineRange;
|
CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange];
|
if (CGRectContainsPoint(lineRect, location) == NO)
|
return nil;
|
|
// Find the word that was touched and call the detection block
|
for (NSDictionary *dictionary in self.linkRanges)
|
{
|
NSRange range = [[dictionary objectForKey:KILabelRangeKey] rangeValue];
|
|
if ((touchedChar >= range.location) && touchedChar < (range.location + range.length))
|
{
|
return dictionary;
|
}
|
}
|
|
return nil;
|
}
|
|
// Applies background color to selected range. Used to hilight touched links
|
- (void)setSelectedRange:(NSRange)range
|
{
|
// Remove the current selection if the selection is changing
|
if (self.selectedRange.length && !NSEqualRanges(self.selectedRange, range))
|
{
|
[_textStorage removeAttribute:NSBackgroundColorAttributeName range:self.selectedRange];
|
}
|
|
// Apply the new selection to the text
|
if (range.length && _selectedLinkBackgroundColor != nil)
|
{
|
[_textStorage addAttribute:NSBackgroundColorAttributeName value:_selectedLinkBackgroundColor range:range];
|
}
|
|
// Save the new range
|
_selectedRange = range;
|
|
[self setNeedsDisplay];
|
}
|
|
- (void)setNumberOfLines:(NSInteger)numberOfLines
|
{
|
[super setNumberOfLines:numberOfLines];
|
|
_textContainer.maximumNumberOfLines = numberOfLines;
|
}
|
|
- (void)setText:(NSString *)text
|
{
|
// Pass the text to the super class first
|
[super setText:text];
|
|
// Update our text store with an attributed string based on the original
|
// label text properties.
|
if (!text)
|
{
|
text = @"";
|
}
|
|
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:[self attributesFromProperties]];
|
[self updateTextStoreWithAttributedString:attributedText];
|
}
|
|
- (void)setAttributedText:(NSAttributedString *)attributedText
|
{
|
// Pass the text to the super class first
|
[super setAttributedText:attributedText];
|
|
[self updateTextStoreWithAttributedString:attributedText];
|
}
|
|
- (void)setSystemURLStyle:(BOOL)systemURLStyle
|
{
|
_systemURLStyle = systemURLStyle;
|
|
// Force refresh
|
self.text = self.text;
|
}
|
|
- (NSDictionary*)attributesForLinkType:(KILinkType)linkType
|
{
|
NSDictionary *attributes = _linkTypeAttributes[@(linkType)];
|
|
if (!attributes)
|
{
|
attributes = @{NSForegroundColorAttributeName : self.tintColor};
|
}
|
|
return attributes;
|
}
|
|
- (void)setAttributes:(NSDictionary*)attributes forLinkType:(KILinkType)linkType
|
{
|
if (attributes)
|
{
|
_linkTypeAttributes[@(linkType)] = attributes;
|
}
|
else
|
{
|
[_linkTypeAttributes removeObjectForKey:@(linkType)];
|
}
|
|
// Force refresh text
|
self.text = self.text;
|
}
|
|
#pragma mark - Text Storage Management
|
|
- (void)updateTextStoreWithText
|
{
|
// Now update our storage from either the attributedString or the plain text
|
if (self.attributedText)
|
{
|
[self updateTextStoreWithAttributedString:self.attributedText];
|
}
|
else if (self.text)
|
{
|
[self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:self.text attributes:[self attributesFromProperties]]];
|
}
|
else
|
{
|
[self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:@"" attributes:[self attributesFromProperties]]];
|
}
|
|
[self setNeedsDisplay];
|
}
|
|
- (void)updateTextStoreWithAttributedString:(NSAttributedString *)attributedString
|
{
|
if (attributedString.length != 0)
|
{
|
attributedString = [KILabel sanitizeAttributedString:attributedString];
|
}
|
|
if (self.isAutomaticLinkDetectionEnabled && (attributedString.length != 0))
|
{
|
self.linkRanges = [self getRangesForLinks:attributedString];
|
attributedString = [self addLinkAttributesToAttributedString:attributedString linkRanges:self.linkRanges];
|
}
|
else
|
{
|
self.linkRanges = nil;
|
}
|
|
if (_textStorage)
|
{
|
// Set the string on the storage
|
[_textStorage setAttributedString:attributedString];
|
}
|
else
|
{
|
// Create a new text storage and attach it correctly to the layout manager
|
_textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
|
[_textStorage addLayoutManager:_layoutManager];
|
[_layoutManager setTextStorage:_textStorage];
|
}
|
}
|
|
// Returns attributed string attributes based on the text properties set on the label.
|
// These are styles that are only applied when NOT using the attributedText directly.
|
- (NSDictionary *)attributesFromProperties
|
{
|
// Setup shadow attributes
|
NSShadow *shadow = shadow = [[NSShadow alloc] init];
|
if (self.shadowColor)
|
{
|
shadow.shadowColor = self.shadowColor;
|
shadow.shadowOffset = self.shadowOffset;
|
}
|
else
|
{
|
shadow.shadowOffset = CGSizeMake(0, -1);
|
shadow.shadowColor = nil;
|
}
|
|
// Setup color attributes
|
UIColor *color = self.textColor;
|
if (!self.isEnabled)
|
{
|
color = [UIColor lightGrayColor];
|
}
|
else if (self.isHighlighted)
|
{
|
color = self.highlightedTextColor;
|
}
|
|
// Setup paragraph attributes
|
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
|
paragraph.alignment = self.textAlignment;
|
|
// Create the dictionary
|
NSDictionary *attributes = @{NSFontAttributeName : self.font,
|
NSForegroundColorAttributeName : color,
|
NSShadowAttributeName : shadow,
|
NSParagraphStyleAttributeName : paragraph,
|
};
|
return attributes;
|
}
|
|
/**
|
* Returns array of ranges for all special words, user handles, hashtags and urls in the specfied
|
* text.
|
*
|
* @param text Text to parse for links
|
*
|
* @return Array of dictionaries describing the links.
|
*/
|
- (NSArray *)getRangesForLinks:(NSAttributedString *)text
|
{
|
NSMutableArray *rangesForLinks = [[NSMutableArray alloc] init];
|
|
if (self.linkDetectionTypes & KILinkTypeOptionUserHandle)
|
{
|
[rangesForLinks addObjectsFromArray:[self getRangesForUserHandles:text.string]];
|
}
|
|
if (self.linkDetectionTypes & KILinkTypeOptionHashtag)
|
{
|
[rangesForLinks addObjectsFromArray:[self getRangesForHashtags:text.string]];
|
}
|
|
if (self.linkDetectionTypes & KILinkTypeOptionURL)
|
{
|
[rangesForLinks addObjectsFromArray:[self getRangesForURLs:self.attributedText]];
|
}
|
|
return rangesForLinks;
|
}
|
|
- (NSArray *)getRangesForUserHandles:(NSString *)text
|
{
|
NSMutableArray *rangesForUserHandles = [[NSMutableArray alloc] init];
|
|
// Setup a regular expression for user handles and hashtags
|
static NSRegularExpression *regex = nil;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
NSError *error = nil;
|
regex = [[NSRegularExpression alloc] initWithPattern:@"(?<!\\w)@([\\w\\_]+)?" options:0 error:&error];
|
});
|
|
// Run the expression and get matches
|
NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)];
|
|
// Add all our ranges to the result
|
for (NSTextCheckingResult *match in matches)
|
{
|
NSRange matchRange = [match range];
|
NSString *matchString = [text substringWithRange:matchRange];
|
|
if (![self ignoreMatch:matchString])
|
{
|
[rangesForUserHandles addObject:@{KILabelLinkTypeKey : @(KILinkTypeUserHandle),
|
KILabelRangeKey : [NSValue valueWithRange:matchRange],
|
KILabelLinkKey : matchString
|
}];
|
}
|
}
|
|
return rangesForUserHandles;
|
}
|
|
- (NSArray *)getRangesForHashtags:(NSString *)text
|
{
|
NSMutableArray *rangesForHashtags = [[NSMutableArray alloc] init];
|
|
// Setup a regular expression for user handles and hashtags
|
static NSRegularExpression *regex = nil;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
NSError *error = nil;
|
regex = [[NSRegularExpression alloc] initWithPattern:@"(?<!\\w)#([\\w\\_]+)?" options:0 error:&error];
|
});
|
|
// Run the expression and get matches
|
NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)];
|
|
// Add all our ranges to the result
|
for (NSTextCheckingResult *match in matches)
|
{
|
NSRange matchRange = [match range];
|
NSString *matchString = [text substringWithRange:matchRange];
|
|
if (![self ignoreMatch:matchString])
|
{
|
[rangesForHashtags addObject:@{KILabelLinkTypeKey : @(KILinkTypeHashtag),
|
KILabelRangeKey : [NSValue valueWithRange:matchRange],
|
KILabelLinkKey : matchString,
|
}];
|
}
|
}
|
|
return rangesForHashtags;
|
}
|
|
|
- (NSArray *)getRangesForURLs:(NSAttributedString *)text
|
{
|
NSMutableArray *rangesForURLs = [[NSMutableArray alloc] init];;
|
|
// Use a data detector to find urls in the text
|
NSError *error = nil;
|
NSDataDetector *detector = [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:&error];
|
|
NSString *plainText = text.string;
|
|
NSArray *matches = [detector matchesInString:plainText
|
options:0
|
range:NSMakeRange(0, text.length)];
|
|
// Add a range entry for every url we found
|
for (NSTextCheckingResult *match in matches)
|
{
|
NSRange matchRange = [match range];
|
|
// If there's a link embedded in the attributes, use that instead of the raw text
|
NSString *realURL = [text attribute:NSLinkAttributeName atIndex:matchRange.location effectiveRange:nil];
|
if (realURL == nil)
|
realURL = [plainText substringWithRange:matchRange];
|
|
if (![self ignoreMatch:realURL])
|
{
|
if ([match resultType] == NSTextCheckingTypeLink)
|
{
|
[rangesForURLs addObject:@{KILabelLinkTypeKey : @(KILinkTypeURL),
|
KILabelRangeKey : [NSValue valueWithRange:matchRange],
|
KILabelLinkKey : realURL,
|
}];
|
}
|
}
|
}
|
|
return rangesForURLs;
|
}
|
|
- (BOOL)ignoreMatch:(NSString*)string
|
{
|
return [_ignoredKeywords containsObject:[string lowercaseString]];
|
}
|
|
- (NSAttributedString *)addLinkAttributesToAttributedString:(NSAttributedString *)string linkRanges:(NSArray *)linkRanges
|
{
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
|
|
for (NSDictionary *dictionary in linkRanges)
|
{
|
NSRange range = [[dictionary objectForKey:KILabelRangeKey] rangeValue];
|
KILinkType linkType = [dictionary[KILabelLinkTypeKey] unsignedIntegerValue];
|
|
NSDictionary *attributes = [self attributesForLinkType:linkType];
|
|
// Use our tint color to hilight the link
|
[attributedString addAttributes:attributes range:range];
|
|
// Add an URL attribute if this is a URL
|
if (_systemURLStyle && ((KILinkType)[dictionary[KILabelLinkTypeKey] unsignedIntegerValue] == KILinkTypeURL))
|
{
|
// Add a link attribute using the stored link
|
[attributedString addAttribute:NSLinkAttributeName value:dictionary[KILabelLinkKey] range:range];
|
}
|
}
|
|
return attributedString;
|
}
|
|
#pragma mark - Layout and Rendering
|
|
- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines
|
{
|
// Use our text container to calculate the bounds required. First save our
|
// current text container setup
|
CGSize savedTextContainerSize = _textContainer.size;
|
NSInteger savedTextContainerNumberOfLines = _textContainer.maximumNumberOfLines;
|
|
// Apply the new potential bounds and number of lines
|
_textContainer.size = bounds.size;
|
_textContainer.maximumNumberOfLines = numberOfLines;
|
|
// Measure the text with the new state
|
CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
|
|
// Position the bounds and round up the size for good measure
|
textBounds.origin = bounds.origin;
|
textBounds.size.width = ceil(textBounds.size.width);
|
textBounds.size.height = ceil(textBounds.size.height);
|
|
if (textBounds.size.height < bounds.size.height)
|
{
|
// Take verical alignment into account
|
CGFloat offsetY = (bounds.size.height - textBounds.size.height) / 2.0;
|
textBounds.origin.y += offsetY;
|
}
|
|
// Restore the old container state before we exit under any circumstances
|
_textContainer.size = savedTextContainerSize;
|
_textContainer.maximumNumberOfLines = savedTextContainerNumberOfLines;
|
|
return textBounds;
|
}
|
|
- (void)drawTextInRect:(CGRect)rect
|
{
|
// Don't call super implementation. Might want to uncomment this out when
|
// debugging layout and rendering problems.
|
// [super drawTextInRect:rect];
|
|
// Calculate the offset of the text in the view
|
NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer];
|
CGPoint glyphsPosition = [self calcGlyphsPositionInView];
|
|
// Drawing code
|
[_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:glyphsPosition];
|
[_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:glyphsPosition];
|
}
|
|
// Returns the XY offset of the range of glyphs from the view's origin
|
- (CGPoint)calcGlyphsPositionInView
|
{
|
CGPoint textOffset = CGPointZero;
|
|
CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
|
textBounds.size.width = ceil(textBounds.size.width);
|
textBounds.size.height = ceil(textBounds.size.height);
|
|
if (textBounds.size.height < self.bounds.size.height)
|
{
|
CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0;
|
textOffset.y = paddingHeight;
|
}
|
|
return textOffset;
|
}
|
|
- (void)setFrame:(CGRect)frame
|
{
|
[super setFrame:frame];
|
|
_textContainer.size = self.bounds.size;
|
}
|
|
- (void)setBounds:(CGRect)bounds
|
{
|
[super setBounds:bounds];
|
|
_textContainer.size = self.bounds.size;
|
}
|
|
- (void)layoutSubviews
|
{
|
[super layoutSubviews];
|
|
// Update our container size when the view frame changes
|
_textContainer.size = self.bounds.size;
|
}
|
|
- (void)setIgnoredKeywords:(NSSet *)ignoredKeywords
|
{
|
NSMutableSet *set = [NSMutableSet setWithCapacity:ignoredKeywords.count];
|
|
[ignoredKeywords enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
|
[set addObject:[obj lowercaseString]];
|
}];
|
|
_ignoredKeywords = [set copy];
|
}
|
|
#pragma mark - Interactions
|
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
_isTouchMoved = NO;
|
|
// Get the info for the touched link if there is one
|
NSDictionary *touchedLink;
|
CGPoint touchLocation = [[touches anyObject] locationInView:self];
|
touchedLink = [self linkAtPoint:touchLocation];
|
|
if (touchedLink)
|
{
|
self.selectedRange = [[touchedLink objectForKey:KILabelRangeKey] rangeValue];
|
}
|
else
|
{
|
[super touchesBegan:touches withEvent:event];
|
}
|
}
|
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
[super touchesMoved:touches withEvent:event];
|
|
_isTouchMoved = YES;
|
}
|
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
[super touchesEnded:touches withEvent:event];
|
|
// If the user dragged their finger we ignore the touch
|
if (_isTouchMoved)
|
{
|
self.selectedRange = NSMakeRange(0, 0);
|
|
return;
|
}
|
|
// Get the info for the touched link if there is one
|
NSDictionary *touchedLink;
|
CGPoint touchLocation = [[touches anyObject] locationInView:self];
|
touchedLink = [self linkAtPoint:touchLocation];
|
|
if (touchedLink)
|
{
|
NSRange range = [[touchedLink objectForKey:KILabelRangeKey] rangeValue];
|
NSString *touchedSubstring = [touchedLink objectForKey:KILabelLinkKey];
|
KILinkType linkType = (KILinkType)[[touchedLink objectForKey:KILabelLinkTypeKey] intValue];
|
|
[self receivedActionForLinkType:linkType string:touchedSubstring range:range];
|
}
|
else
|
{
|
[super touchesBegan:touches withEvent:event];
|
}
|
|
self.selectedRange = NSMakeRange(0, 0);
|
}
|
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
|
{
|
[super touchesCancelled:touches withEvent:event];
|
|
// Make sure we don't leave a selection when the touch is cancelled
|
self.selectedRange = NSMakeRange(0, 0);
|
}
|
|
- (void)receivedActionForLinkType:(KILinkType)linkType string:(NSString*)string range:(NSRange)range
|
{
|
switch (linkType)
|
{
|
case KILinkTypeUserHandle:
|
if (_userHandleLinkTapHandler)
|
{
|
_userHandleLinkTapHandler(self, string, range);
|
}
|
break;
|
|
case KILinkTypeHashtag:
|
if (_hashtagLinkTapHandler)
|
{
|
_hashtagLinkTapHandler(self, string, range);
|
}
|
break;
|
|
case KILinkTypeURL:
|
if (_urlLinkTapHandler)
|
{
|
_urlLinkTapHandler(self, string, range);
|
}
|
break;
|
}
|
}
|
|
#pragma mark - Layout manager delegate
|
|
-(BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex
|
{
|
// Don't allow line breaks inside URLs
|
NSRange range;
|
NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range];
|
|
return !(linkURL && (charIndex > range.location) && (charIndex <= NSMaxRange(range)));
|
}
|
|
+ (NSAttributedString *)sanitizeAttributedString:(NSAttributedString *)attributedString
|
{
|
// Setup paragraph alignement properly. IB applies the line break style
|
// to the attributed string. The problem is that the text container then
|
// breaks at the first line of text. If we set the line break to wrapping
|
// then the text container defines the break mode and it works.
|
// NOTE: This is either an Apple bug or something I've misunderstood.
|
|
// Get the current paragraph style. IB only allows a single paragraph so
|
// getting the style of the first char is fine.
|
NSRange range;
|
NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:&range];
|
|
if (paragraphStyle == nil)
|
{
|
return attributedString;
|
}
|
|
// Remove the line breaks
|
NSMutableParagraphStyle *mutableParagraphStyle = [paragraphStyle mutableCopy];
|
mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
|
|
// Apply new style
|
NSMutableAttributedString *restyled = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
|
[restyled addAttribute:NSParagraphStyleAttributeName value:mutableParagraphStyle range:NSMakeRange(0, restyled.length)];
|
|
return restyled;
|
}
|
|
@end
|