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