New file |
| | |
| | | /*********************************************************************************** |
| | | * |
| | | * 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 |