Project import
diff --git a/TTTAttributedLabel.h b/TTTAttributedLabel.h new file mode 100755 index 0000000..1b2f7e7 --- /dev/null +++ b/TTTAttributedLabel.h
@@ -0,0 +1,283 @@ +// TTTAttributedLabel.h +// +// Copyright (c) 2011 Mattt Thompson (http://mattt.me) +// +// 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 <UIKit/UIKit.h> +#import <CoreText/CoreText.h> + +/** + Vertical alignment for text in a label whose bounds are larger than its text bounds + */ +typedef enum { + TTTAttributedLabelVerticalAlignmentCenter = 0, + TTTAttributedLabelVerticalAlignmentTop = 1, + TTTAttributedLabelVerticalAlignmentBottom = 2, +} TTTAttributedLabelVerticalAlignment; + +@protocol TTTAttributedLabelDelegate; + +// Override UILabel @property to accept both NSString and NSAttributedString +@protocol TTTAttributedLabel <NSObject> +@property (nonatomic, copy) id text; +@end + +/** + `TTTAttributedLabel` is a drop-in replacement for `UILabel` that supports `NSAttributedString`, as well as automatically-detected and manually-added links to URLs, addresses, phone numbers, and dates. + + # Differences Between `TTTAttributedLabel` and `UILabel` + + For the most part, `TTTAttributedLabel` behaves just like `UILabel`. The following are notable exceptions, in which `TTTAttributedLabel` properties may act differently: + + - `text` - This property now takes an `id` type argument, which can either be a kind of `NSString` or `NSAttributedString` (mutable or immutable in both cases) + - `lineBreakMode` - This property displays only the first line when the value is `UILineBreakModeHeadTruncation`, `UILineBreakModeTailTruncation`, or `UILineBreakModeMiddleTruncation` + - `adjustsFontsizeToFitWidth` - This property is effective for any value of `numberOfLines` greater than zero + */ +@interface TTTAttributedLabel : UILabel <TTTAttributedLabel> { +@private + NSAttributedString *_attributedText; + CTFramesetterRef _framesetter; + BOOL _needsFramesetter; + + id _delegate; + UIDataDetectorTypes _dataDetectorTypes; + NSArray *_links; + NSDictionary *_linkAttributes; + + CGFloat _shadowRadius; + + CGFloat _leading; + CGFloat _lineHeightMultiple; + CGFloat _firstLineIndent; + UIEdgeInsets _textInsets; + TTTAttributedLabelVerticalAlignment _verticalAlignment; + + BOOL _userInteractionDisabled; +} + +///----------------------------- +/// @name Accessing the Delegate +///----------------------------- + +/** + The receiver's delegate. + + @discussion A `TTTAttributedLabel` delegate responds to messages sent by tapping on links in the label. You can use the delegate to respond to links referencing a URL, address, phone number, date, or date with a specified time zone and duration. + */ +@property (nonatomic, assign) id <TTTAttributedLabelDelegate> delegate; + +///-------------------------------------------- +/// @name Detecting, Accessing, & Styling Links +///-------------------------------------------- + +/** + A bitmask of `UIDataDetectorTypes` which are used to automatically detect links in the label text. This is `UIDataDetectorTypeNone` by default. + + @warning You must specify `dataDetectorTypes` before setting the `text`, with either `setText:` or `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`. + */ +@property (nonatomic, assign) UIDataDetectorTypes dataDetectorTypes; + +/** + An array of `NSTextCheckingResult` objects for links detected or manually added to the label text. + */ +@property (readonly, nonatomic, retain) NSArray *links; + +/** + A dictionary containing the `NSAttributedString` attributes to be applied to links detected or manually added to the label text. The default link style is blue and underlined. + + @warning You must specify `linkAttributes` before setting autodecting or manually-adding links for these attributes to be applied. + */ +@property (nonatomic, retain) NSDictionary *linkAttributes; + +///--------------------------------------- +/// @name Acccessing Text Style Attributes +///--------------------------------------- + +/** + The shadow blur radius for the label. A value of 0 indicates no blur, while larger values produce correspondingly larger blurring. This value must not be negative. The default value is 0. + */ +@property (nonatomic, assign) CGFloat shadowRadius; + +///-------------------------------------------- +/// @name Acccessing Paragraph Style Attributes +///-------------------------------------------- + +/** + The distance, in points, from the leading margin of a frame to the beginning of the paragraph's first line. This value is always nonnegative, and is 0.0 by default. + */ +@property (nonatomic, assign) CGFloat firstLineIndent; + +/** + The space in points added between lines within the paragraph. This value is always nonnegative and is 0.0 by default. + */ +@property (nonatomic, assign) CGFloat leading; + +/** + The line height multiple. This value is 0.0 by default. + */ +@property (nonatomic, assign) CGFloat lineHeightMultiple; + +/** + The distance, in points, from the margin to the text container. This value is `UIEdgeInsetsZero` by default. + + @discussion The `UIEdgeInset` members correspond to paragraph style properties rather than a particular geometry, and can change depending on the writing direction. + + ## `UIEdgeInset` Member Correspondence With `CTParagraphStyleSpecifier` Values: + + - `top`: `kCTParagraphStyleSpecifierParagraphSpacingBefore` + - `left`: `kCTParagraphStyleSpecifierHeadIndent` + - `bottom`: `kCTParagraphStyleSpecifierParagraphSpacing` + - `right`: `kCTParagraphStyleSpecifierTailIndent` + + */ +@property (nonatomic, assign) UIEdgeInsets textInsets; + +/** + The vertical text alignment for the label, for when the frame size is greater than the text rect size. The vertical alignment is `TTTAttributedLabelVerticalAlignmentCenter` by default. + */ +@property (nonatomic, assign) TTTAttributedLabelVerticalAlignment verticalAlignment; + + +///---------------------------------- +/// @name Setting the Text Attributes +///---------------------------------- + +/** + Sets the text displayed by the label. + + @param text An `NSString` or `NSAttributedString` object to be displayed by the label. If the specified text is an `NSString`, the label will display the text like a `UILabel`, inheriting the text styles of the label. If the specified text is an `NSAttributedString`, the label text styles will be overridden by the styles specified in the attributed string. + + @discussion This method overrides `UILabel -setText:` to accept both `NSString` and `NSAttributedString` objects. This string is `nil` by default. + */ +- (void)setText:(id)text; + +/** + Sets the text displayed by the label, after configuring an attributed string containing the text attributes inherited from the label in a block. + + @param text An `NSString` or `NSAttributedString` object to be displayed by the label. + @param block A block object that returns an `NSMutableAttributedString` object and takes a single argument, which is an `NSMutableAttributedString` object with the text from the first parameter, and the text attributes inherited from the label text styles. For example, if you specified the `font` of the label to be `[UIFont boldSystemFontOfSize:14]` and `textColor` to be `[UIColor redColor]`, the `NSAttributedString` argument of the block would be contain the `NSAttributedString` attribute equivalents of those properties. In this block, you can set further attributes on particular ranges. + + @discussion This string is `nil` by default. + */ +- (void)setText:(id)text afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString *(^)(NSMutableAttributedString *mutableAttributedString))block; + +///------------------- +/// @name Adding Links +///------------------- + +/** + Adds a link to a URL for a specified range in the label text. + + @param url The url to be linked to + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + */ +- (void)addLinkToURL:(NSURL *)url withRange:(NSRange)range; + +/** + Adds a link to an address for a specified range in the label text. + + @param addressComponents A dictionary of address components for the address to be linked to + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @discussion The address component dictionary keys are described in `NSTextCheckingResult`'s "Keys for Address Components." + */ +- (void)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range; + +/** + Adds a link to a phone number for a specified range in the label text. + + @param phoneNumber The phone number to be linked to. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + */ +- (void)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range; + +/** + Adds a link to a date for a specified range in the label text. + + @param date The date to be linked to. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + */ +- (void)addLinkToDate:(NSDate *)date withRange:(NSRange)range; + +/** + Adds a link to a date with a particular time zone and duration for a specified range in the label text. + + @param date The date to be linked to. + @param timeZone The time zone of the specified date. + @param duration The duration, in seconds from the specified date. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + */ +- (void)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range; + +@end + +/** + The `TTTAttributedLabelDelegate` protocol defines the messages sent to an attributed label delegate when links are tapped. All of the methods of this protocol are optional. + */ +@protocol TTTAttributedLabelDelegate <NSObject> + +///----------------------------------- +/// @name Responding to Link Selection +///----------------------------------- +@optional + +/** + Tells the delegate that the user did select a link to a URL. + + @param label The label whose link was selected. + @param url The URL for the selected link. + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url; + +/** + Tells the delegate that the user did select a link to an address. + + @param label The label whose link was selected. + @param addressComponents The components of the address for the selected link. + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithAddress:(NSDictionary *)addressComponents; + +/** + Tells the delegate that the user did select a link to a phone number. + + @param label The label whose link was selected. + @param phoneNumber The phone number for the selected link. + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithPhoneNumber:(NSString *)phoneNumber; + +/** + Tells the delegate that the user did select a link to a date. + + @param label The label whose link was selected. + @param date The datefor the selected link. + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithDate:(NSDate *)date; + +/** + Tells the delegate that the user did select a link to a date with a time zone and duration. + + @param label The label whose link was selected. + @param date The date for the selected link. + @param timeZone The time zone of the date for the selected link. + @param duration The duration, in seconds from the date for the selected link. + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration; +@end +
diff --git a/TTTAttributedLabel.m b/TTTAttributedLabel.m new file mode 100755 index 0000000..bc80d39 --- /dev/null +++ b/TTTAttributedLabel.m
@@ -0,0 +1,535 @@ +// TTTAttributedLabel.m +// +// Copyright (c) 2011 Mattt Thompson (http://mattt.me) +// +// 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 "TTTAttributedLabel.h" + +#define kTTTLineBreakWordWrapTextWidthScalingFactor (M_PI / M_E) + +static inline CTTextAlignment CTTextAlignmentFromUITextAlignment(UITextAlignment alignment) { + switch (alignment) { + case UITextAlignmentLeft: return kCTLeftTextAlignment; + case UITextAlignmentCenter: return kCTCenterTextAlignment; + case UITextAlignmentRight: return kCTRightTextAlignment; + default: return kCTNaturalTextAlignment; + } +} + +static inline CTLineBreakMode CTLineBreakModeFromUILineBreakMode(UILineBreakMode lineBreakMode) { + switch (lineBreakMode) { + case UILineBreakModeWordWrap: return kCTLineBreakByWordWrapping; + case UILineBreakModeCharacterWrap: return kCTLineBreakByCharWrapping; + case UILineBreakModeClip: return kCTLineBreakByClipping; + case UILineBreakModeHeadTruncation: return kCTLineBreakByTruncatingHead; + case UILineBreakModeTailTruncation: return kCTLineBreakByTruncatingTail; + case UILineBreakModeMiddleTruncation: return kCTLineBreakByTruncatingMiddle; + default: return 0; + } +} + +static inline NSTextCheckingType NSTextCheckingTypeFromUIDataDetectorType(UIDataDetectorTypes dataDetectorType) { + NSTextCheckingType textCheckingType = 0; + if (dataDetectorType & UIDataDetectorTypeAddress) { + textCheckingType |= NSTextCheckingTypeAddress; + } + + if (dataDetectorType & UIDataDetectorTypeCalendarEvent) { + textCheckingType |= NSTextCheckingTypeDate; + } + + if (dataDetectorType & UIDataDetectorTypeLink) { + textCheckingType |= NSTextCheckingTypeLink; + } + + if (dataDetectorType & UIDataDetectorTypePhoneNumber) { + textCheckingType |= NSTextCheckingTypePhoneNumber; + } + + return textCheckingType; +} + +static inline NSDictionary * NSAttributedStringAttributesFromLabel(TTTAttributedLabel *label) { + NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary]; + + CTFontRef font = CTFontCreateWithName((CFStringRef)label.font.fontName, label.font.pointSize, NULL); + [mutableAttributes setObject:(id)font forKey:(NSString *)kCTFontAttributeName]; + CFRelease(font); + + [mutableAttributes setObject:(id)[label.textColor CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + + CTTextAlignment alignment = CTTextAlignmentFromUITextAlignment(label.textAlignment); + CTLineBreakMode lineBreakMode = CTLineBreakModeFromUILineBreakMode(label.lineBreakMode); + CGFloat lineSpacing = label.leading; + CGFloat lineHeightMultiple = label.lineHeightMultiple; + CGFloat topMargin = label.textInsets.top; + CGFloat bottomMargin = label.textInsets.bottom; + CGFloat leftMargin = label.textInsets.left; + CGFloat rightMargin = label.textInsets.right; + CGFloat firstLineIndent = label.firstLineIndent + leftMargin; + CTParagraphStyleSetting paragraphStyles[9] = { + {.spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof(CTTextAlignment), .value = (const void *)&alignment}, + {.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void *)&lineBreakMode}, + {.spec = kCTParagraphStyleSpecifierLineSpacing, .valueSize = sizeof(CGFloat), .value = (const void *)&lineSpacing}, + {.spec = kCTParagraphStyleSpecifierLineHeightMultiple, .valueSize = sizeof(CGFloat), .value = (const void *)&lineHeightMultiple}, + {.spec = kCTParagraphStyleSpecifierFirstLineHeadIndent, .valueSize = sizeof(CGFloat), .value = (const void *)&firstLineIndent}, + {.spec = kCTParagraphStyleSpecifierParagraphSpacingBefore, .valueSize = sizeof(CGFloat), .value = (const void *)&topMargin}, + {.spec = kCTParagraphStyleSpecifierParagraphSpacing, .valueSize = sizeof(CGFloat), .value = (const void *)&bottomMargin}, + {.spec = kCTParagraphStyleSpecifierHeadIndent, .valueSize = sizeof(CGFloat), .value = (const void *)&leftMargin}, + {.spec = kCTParagraphStyleSpecifierTailIndent, .valueSize = sizeof(CGFloat), .value = (const void *)&rightMargin}, + }; + CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphStyles, 9); + [mutableAttributes setObject:(id)paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName]; + CFRelease(paragraphStyle); + + return [NSDictionary dictionaryWithDictionary:mutableAttributes]; +} + +static inline NSAttributedString * NSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale, CGFloat minimumFontSize) { + NSMutableAttributedString *mutableAttributedString = [[attributedString mutableCopy] autorelease]; + [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { + CTFontRef font = (CTFontRef)value; + if (font) { + CGFloat scaledFontSize = floorf(CTFontGetSize(font) * scale); + CTFontRef scaledFont = CTFontCreateCopyWithAttributes(font, fmaxf(scaledFontSize, minimumFontSize), NULL, NULL); + CFAttributedStringSetAttribute((CFMutableAttributedStringRef)mutableAttributedString, CFRangeMake(range.location, range.length), kCTFontAttributeName, scaledFont); + } + }]; + + return mutableAttributedString; +} + +@interface TTTAttributedLabel () +@property (readwrite, nonatomic, copy) NSAttributedString *attributedText; +@property (readwrite, nonatomic, assign) CTFramesetterRef framesetter; +@property (readwrite, nonatomic, assign) CTFramesetterRef highlightFramesetter; +@property (readwrite, nonatomic, retain) NSArray *links; + +- (id)initCommon; +- (void)setNeedsFramesetter; +- (NSArray *)detectedLinksInString:(NSString *)string range:(NSRange)range error:(NSError **)error; +- (NSTextCheckingResult *)linkAtCharacterIndex:(CFIndex)idx; +- (NSTextCheckingResult *)linkAtPoint:(CGPoint)p; +- (NSUInteger)characterIndexAtPoint:(CGPoint)p; +- (void)drawFramesetter:(CTFramesetterRef)framesetter textRange:(CFRange)textRange inRect:(CGRect)rect context:(CGContextRef)c; +@end + +@implementation TTTAttributedLabel +@dynamic text; +@synthesize attributedText = _attributedText; +@synthesize framesetter = _framesetter; +@synthesize highlightFramesetter = _highlightFramesetter; +@synthesize delegate = _delegate; +@synthesize dataDetectorTypes = _dataDetectorTypes; +@synthesize links = _links; +@synthesize linkAttributes = _linkAttributes; +@synthesize shadowRadius = _shadowRadius; +@synthesize leading = _leading; +@synthesize lineHeightMultiple = _lineHeightMultiple; +@synthesize firstLineIndent = _firstLineIndent; +@synthesize textInsets = _textInsets; +@synthesize verticalAlignment = _verticalAlignment; + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { + return nil; + } + + return [self initCommon]; +} + +- (id)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (!self) { + return nil; + } + + return [self initCommon]; +} + +- (id)initCommon { + self.dataDetectorTypes = UIDataDetectorTypeNone; + self.links = [NSArray array]; + + NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary]; + [mutableLinkAttributes setValue:(id)[[UIColor blueColor] CGColor] forKey:(NSString*)kCTForegroundColorAttributeName]; + [mutableLinkAttributes setValue:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes]; + + self.textInsets = UIEdgeInsetsZero; + + return self; +} + +- (void)dealloc { + if (_framesetter) CFRelease(_framesetter); + if (_highlightFramesetter) CFRelease(_highlightFramesetter); + + [_attributedText release]; + [_links release]; + [_linkAttributes release]; + [super dealloc]; +} + +#pragma mark - + +- (void)setAttributedText:(NSAttributedString *)text { + if ([text isEqualToAttributedString:self.attributedText]) { + return; + } + + [self willChangeValueForKey:@"attributedText"]; + [_attributedText release]; + _attributedText = [text copy]; + [self didChangeValueForKey:@"attributedText"]; + + [self setNeedsFramesetter]; +} + +- (void)setNeedsFramesetter { + _needsFramesetter = YES; +} + +- (CTFramesetterRef)framesetter { + if (_needsFramesetter) { + @synchronized(self) { + if (_framesetter) CFRelease(_framesetter); + if (_highlightFramesetter) CFRelease(_highlightFramesetter); + + self.framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedText); + self.highlightFramesetter = nil; + _needsFramesetter = NO; + } + } + + return _framesetter; +} + +- (BOOL)isUserInteractionEnabled { + return !_userInteractionDisabled && [self.links count] > 0; +} + +- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled { + _userInteractionDisabled = !userInteractionEnabled; +} + +- (BOOL)isExclusiveTouch { + return [self.links count] > 0; +} + +#pragma mark - + +- (NSArray *)detectedLinksInString:(NSString *)string range:(NSRange)range error:(NSError **)error { + if (!string) { + return [NSArray array]; + } + NSMutableArray *mutableLinks = [NSMutableArray array]; + NSDataDetector *dataDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeFromUIDataDetectorType(self.dataDetectorTypes) error:error]; + [dataDetector enumerateMatchesInString:string options:0 range:range usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + [mutableLinks addObject:result]; + }]; + + return [NSArray arrayWithArray:mutableLinks]; +} + +- (void)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result { + self.links = [self.links arrayByAddingObject:result]; + + if (self.linkAttributes) { + NSMutableAttributedString *mutableAttributedString = [[[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText] autorelease]; + [mutableAttributedString addAttributes:self.linkAttributes range:result.range]; + self.attributedText = mutableAttributedString; + } +} + +- (void)addLinkToURL:(NSURL *)url withRange:(NSRange)range { + [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]]; +} + +- (void)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range { + [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]]; +} + +- (void)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range { + [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]]; +} + +- (void)addLinkToDate:(NSDate *)date withRange:(NSRange)range { + [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]]; +} + +- (void)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range { + [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]]; +} + +#pragma mark - + +- (NSTextCheckingResult *)linkAtCharacterIndex:(CFIndex)idx { + for (NSTextCheckingResult *result in self.links) { + NSRange range = result.range; + if (range.location <= idx && idx <= range.location + range.length) { + return result; + } + } + + return nil; +} + +- (NSTextCheckingResult *)linkAtPoint:(CGPoint)p { + CFIndex idx = [self characterIndexAtPoint:p]; + return [self linkAtCharacterIndex:idx]; +} + +- (NSUInteger)characterIndexAtPoint:(CGPoint)p { + if (!CGRectContainsPoint(self.bounds, p)) { + return NSNotFound; + } + + CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines]; + if (!CGRectContainsPoint(textRect, p)) { + return NSNotFound; + } + + // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left) + p = CGPointMake(p.x, textRect.size.height - p.y); + + CFIndex idx = NSNotFound; + CGMutablePathRef path = CGPathCreateMutable(); + CGPathAddRect(path, NULL, textRect); + CTFrameRef frame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, [self.attributedText length]), path, NULL); + CFArrayRef lines = CTFrameGetLines(frame); + NSUInteger numberOfLines = CFArrayGetCount(lines); + CGPoint lineOrigins[numberOfLines]; + CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins); + NSUInteger lineIndex; + + for (lineIndex = 0; lineIndex < (numberOfLines - 1); lineIndex++) { + CGPoint lineOrigin = lineOrigins[lineIndex]; + if (lineOrigin.y < p.y) { + break; + } + } + + CGPoint lineOrigin = lineOrigins[lineIndex]; + CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex); + // Convert CT coordinates to line-relative coordinates + CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y); + idx = CTLineGetStringIndexForPosition(line, relativePoint); + + CFRelease(frame); + CFRelease(path); + + return idx; +} + +- (void)drawFramesetter:(CTFramesetterRef)framesetter textRange:(CFRange)textRange inRect:(CGRect)rect context:(CGContextRef)c { + CGMutablePathRef path = CGPathCreateMutable(); + + CGPathAddRect(path, NULL, rect); + CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL); + + if (self.numberOfLines == 0) { + CTFrameDraw(frame, c); + } else { + CFArrayRef lines = CTFrameGetLines(frame); + NSUInteger numberOfLines = MIN(self.numberOfLines, CFArrayGetCount(lines)); + + CGPoint lineOrigins[numberOfLines]; + CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins); + + for (NSUInteger lineIndex = 0; lineIndex < numberOfLines; lineIndex++) { + CGPoint lineOrigin = lineOrigins[lineIndex]; + CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y); + CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex); + CTLineDraw(line, c); + } + } + + CFRelease(frame); + CFRelease(path); +} + +#pragma mark - TTTAttributedLabel + +- (void)setText:(id)text { + if ([text isKindOfClass:[NSString class]]) { + [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil]; + } else { + self.attributedText = text; + } + + self.links = [NSArray array]; + if (self.dataDetectorTypes != UIDataDetectorTypeNone) { + for (NSTextCheckingResult *result in [self detectedLinksInString:[self.attributedText string] range:NSMakeRange(0, [text length]) error:nil]) { + [self addLinkWithTextCheckingResult:result]; + } + } + + [super setText:[self.attributedText string]]; +} + +- (void)setText:(id)text afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString *(^)(NSMutableAttributedString *mutableAttributedString))block { + if ([text isKindOfClass:[NSString class]]) { + self.attributedText = [[[NSAttributedString alloc] initWithString:text] autorelease]; + } + + NSMutableAttributedString *mutableAttributedString = [[[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText] autorelease]; + [mutableAttributedString addAttributes:NSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])]; + + if (block) { + [self setText:block(mutableAttributedString)]; + } else { + [self setText:mutableAttributedString]; + } +} + +#pragma mark - UILabel + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + [self setNeedsDisplay]; +} + +- (void)drawTextInRect:(CGRect)rect { + if (!self.attributedText) { + [super drawTextInRect:rect]; + return; + } + + NSAttributedString *originalAttributedText = nil; + // Adjust the font size to fit width, if necessarry + if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) { + CGFloat textWidth = [self sizeThatFits:CGSizeZero].width; + CGFloat availableWidth = self.frame.size.width * self.numberOfLines; + if (self.numberOfLines > 1 && self.lineBreakMode == UILineBreakModeWordWrap) { + textWidth *= kTTTLineBreakWordWrapTextWidthScalingFactor; + } + + if (textWidth > availableWidth && textWidth > 0.0f) { + originalAttributedText = [[self.attributedText copy] autorelease]; + self.text = NSAttributedStringByScalingFontSize(self.attributedText, availableWidth / textWidth, self.minimumFontSize); + } + } + + CGContextRef c = UIGraphicsGetCurrentContext(); + CGContextSetTextMatrix(c, CGAffineTransformIdentity); + + // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different) + CGContextTranslateCTM(c, 0.0f, self.bounds.size.height); + CGContextScaleCTM(c, 1.0f, -1.0f); + + CGRect textRect = rect; + CFRange textRange = CFRangeMake(0, [self.attributedText length]); + CFRange fitRange; + + // First, adjust the text to be in the center vertically, if the text size is smaller than the drawing rect + CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, textRange, NULL, textRect.size, &fitRange); + + if (textSize.height < textRect.size.height) { + CGFloat yOffset = 0.0f; + switch (self.verticalAlignment) { + case TTTAttributedLabelVerticalAlignmentTop: + break; + case TTTAttributedLabelVerticalAlignmentCenter: + yOffset = floorf((textRect.size.height - textSize.height) / 2.0f); + break; + case TTTAttributedLabelVerticalAlignmentBottom: + yOffset = (textRect.size.height - textSize.height); + break; + } + + textRect.origin = CGPointMake(textRect.origin.x, textRect.origin.y + yOffset); + textRect.size = CGSizeMake(textRect.size.width, textRect.size.height - yOffset); + } + + // Second, trace the shadow before the actual text, if we have one + if (self.shadowColor && !self.highlighted) { + CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, [self.shadowColor CGColor]); + } + + // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one) + if (self.highlightedTextColor && self.highlighted) { + if (!self.highlightFramesetter) { + NSMutableAttributedString *mutableAttributedString = [[self.attributedText mutableCopy] autorelease]; + [mutableAttributedString addAttribute:(NSString *)kCTForegroundColorAttributeName value:(id)[self.highlightedTextColor CGColor] range:NSMakeRange(0, mutableAttributedString.length)]; + self.highlightFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mutableAttributedString); + } + + [self drawFramesetter:self.highlightFramesetter textRange:textRange inRect:textRect context:c]; + } else { + [self drawFramesetter:self.framesetter textRange:textRange inRect:textRect context:c]; + } + + // If we adjusted the font size, set it back to its original size + if (originalAttributedText) { + self.text = originalAttributedText; + } +} + +#pragma mark - UIControl + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + UITouch *touch = [touches anyObject]; + NSTextCheckingResult *result = [self linkAtPoint:[touch locationInView:self]]; + + if (result && self.delegate) { + switch (result.resultType) { + case NSTextCheckingTypeLink: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) { + [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL]; + } + break; + case NSTextCheckingTypeAddress: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) { + [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents]; + } + break; + case NSTextCheckingTypePhoneNumber: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) { + [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber]; + } + break; + case NSTextCheckingTypeDate: + if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:timeZone:duration:)]) { + [self.delegate attributedLabel:self didSelectLinkWithDate:result.date timeZone:result.timeZone duration:result.duration]; + } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) { + [self.delegate attributedLabel:self didSelectLinkWithDate:result.date]; + } + break; + } + } else { + [self.nextResponder touchesBegan:touches withEvent:event]; + } +} + +#pragma mark - UIView + +- (CGSize)sizeThatFits:(CGSize)size { + if (!self.attributedText) { + return [super sizeThatFits:size]; + } + + CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0, [self.attributedText length]), NULL, CGSizeMake(size.width, CGFLOAT_MAX), NULL); + + return CGSizeMake(ceilf(suggestedSize.width), ceilf(suggestedSize.height)); +} + +@end