Project import
diff --git a/TTTAttributedLabel.h b/TTTAttributedLabel.h
new file mode 100755
index 0000000..e43315e
--- /dev/null
+++ b/TTTAttributedLabel.h
@@ -0,0 +1,330 @@
+// 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;
+
+/**
+ Determines whether the text to which this attribute applies has a strikeout drawn through itself.
+ */
+extern NSString * const kTTTStrikeOutAttributeName;
+
+/**
+ The background fill color. Value must be a `CGColorRef`. Default value is `nil` (no fill).
+ */
+extern NSString * const kTTTBackgroundFillColorAttributeName;
+
+/**
+ The background stroke color. Value must be a `CGColorRef`. Default value is `nil` (no stroke).
+ */
+extern NSString * const kTTTBackgroundStrokeColorAttributeName;
+
+/**
+ The background stroke line width. Value must be an `NSNumber`. Default value is `1.0f`.
+ */
+extern NSString * const kTTTBackgroundLineWidthAttributeName;
+
+/**
+ The background corner radius. Value must be an `NSNumber`. Default value is `5.0f`.
+ */
+extern NSString * const kTTTBackgroundCornerRadiusAttributeName;
+
+@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` - Supported in iOS 5 and greater, this property is effective for any value of `numberOfLines` greater than zero. In iOS 4, setting `numberOfLines` to a value greater than 1 with `adjustsFontSizeToFitWidth` set to `YES` may cause `sizeToFit` to execute indefinitely.
+
+ Any properties affecting text or paragraph styling, such as `shadowRadius` or `firstLineIndent` will only apply when text is set with an `NSString`. If the text is set with an `NSAttributedString`, these properties will not apply.
+
+ @warning Any properties changed on the label after setting the text will not be reflected until a subsequent call to `setText:` or `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`. This is to say, order of operations matters in this case. For example, if the label text color is originally black when the text is set, changing the text color to red will have no effect on the display of the label until the text is set once again.
+ */
+@interface TTTAttributedLabel : UILabel <TTTAttributedLabel, UIGestureRecognizerDelegate>
+
+///-----------------------------
+/// @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, unsafe_unretained) 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, strong) 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, strong) NSDictionary *linkAttributes;
+
+/**
+ A dictionary containing the `NSAttributedString` attributes to be applied to links when they are in the active state. Supply `nil` or an empty dictionary to opt out of active link styling. The default active link style is red and underlined.
+ */
+@property (nonatomic, strong) NSDictionary *activeLinkAttributes;
+
+///---------------------------------------
+/// @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 Accessing the Text Attributes
+///----------------------------------
+
+/**
+ A copy of the label's current attributedText. This returns `nil` if an attributed string has never been set on the label.
+ */
+@property (readwrite, nonatomic, copy) NSAttributedString *attributedText;
+
+///-------------------
+/// @name Adding Links
+///-------------------
+
+/**
+ Adds a link to an `NSTextCheckingResult`.
+
+ @param result An `NSTextCheckingResult` representing the link's location and type.
+ */
+- (void)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result;
+
+/**
+ Adds a link to an `NSTextCheckingResult`.
+
+ @param result An `NSTextCheckingResult` representing the link's location and type.
+ @param attributes The attributes to be added to the text in the range of the specified link. If `nil`, no attributes are added.
+ */
+- (void)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes;
+
+/**
+ 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;
+
+/**
+ Tells the delegate that the user did select a link to a text checking result.
+
+ @discussion This method is called if no other delegate method was called, which can occur by either now implementing the method in `TTTAttributedLabelDelegate` corresponding to a particular link, or the link was added by passing an instance of a custom `NSTextCheckingResult` subclass into `-addLinkWithTextCheckingResult:`.
+
+ @param label The label whose link was selected.
+ @param result The custom text checking result.
+ */
+- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithTextCheckingResult:(NSTextCheckingResult *)result;
+
+@end
diff --git a/TTTAttributedLabel.m b/TTTAttributedLabel.m
new file mode 100755
index 0000000..d03b96d
--- /dev/null
+++ b/TTTAttributedLabel.m
@@ -0,0 +1,967 @@
+// 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)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+NSString * const kTTTStrikeOutAttributeName = @"TTTStrikeOutAttribute";
+NSString * const kTTTBackgroundFillColorAttributeName = @"TTTBackgroundFillColor";
+NSString * const kTTTBackgroundStrokeColorAttributeName = @"TTTBackgroundStrokeColor";
+NSString * const kTTTBackgroundLineWidthAttributeName = @"TTTBackgroundLineWidth";
+NSString * const kTTTBackgroundCornerRadiusAttributeName = @"TTTBackgroundCornerRadius";
+
+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((__bridge CFStringRef)label.font.fontName, label.font.pointSize, NULL);
+ [mutableAttributes setObject:(__bridge id)font forKey:(NSString *)kCTFontAttributeName];
+ CFRelease(font);
+
+ [mutableAttributes setObject:(id)[label.textColor CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
+
+ CTTextAlignment alignment = CTTextAlignmentFromUITextAlignment(label.textAlignment);
+ CGFloat lineSpacing = label.leading;
+ CGFloat lineSpacingAdjustment = label.font.lineHeight - label.font.ascender + label.font.descender;
+ 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;
+
+ CTLineBreakMode lineBreakMode;
+ if (label.numberOfLines != 1) {
+ lineBreakMode = CTLineBreakModeFromUILineBreakMode(UILineBreakModeWordWrap);
+ } else {
+ lineBreakMode = CTLineBreakModeFromUILineBreakMode(label.lineBreakMode);
+ }
+
+ CTParagraphStyleSetting paragraphStyles[10] = {
+ {.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 = kCTParagraphStyleSpecifierLineSpacingAdjustment, .valueSize = sizeof (CGFloat), .value = (const void *)&lineSpacingAdjustment},
+ {.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, 10);
+ [mutableAttributes setObject:(__bridge 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];
+ [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
+ CTFontRef font = (__bridge CTFontRef)value;
+ if (font) {
+ CGFloat scaledFontSize = floorf(CTFontGetSize(font) * scale);
+ CTFontRef scaledFont = CTFontCreateCopyWithAttributes(font, fmaxf(scaledFontSize, minimumFontSize), NULL, NULL);
+ CFAttributedStringSetAttribute((__bridge CFMutableAttributedStringRef)mutableAttributedString, CFRangeMake(range.location, range.length), kCTFontAttributeName, scaledFont);
+ CFRelease(scaledFont);
+ }
+ }];
+
+ return mutableAttributedString;
+}
+
+static inline NSAttributedString * NSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) {
+ if (!color) {
+ return attributedString;
+ }
+
+ CGColorRef colorRef = color.CGColor;
+ NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
+ [mutableAttributedString enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
+ CFBooleanRef usesColorFromContext = (__bridge CFBooleanRef)value;
+ if (usesColorFromContext && CFBooleanGetValue(usesColorFromContext)) {
+ CFRange updateRange = CFRangeMake(range.location, range.length);
+ CFAttributedStringSetAttribute((__bridge CFMutableAttributedStringRef)mutableAttributedString, updateRange, kCTForegroundColorAttributeName, colorRef);
+ CFAttributedStringRemoveAttribute((__bridge CFMutableAttributedStringRef)mutableAttributedString, updateRange, kCTForegroundColorFromContextAttributeName);
+ }
+ }];
+
+ return mutableAttributedString;
+}
+
+@interface TTTAttributedLabel ()
+@property (readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText;
+@property (readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText;
+@property (readwrite, nonatomic, assign) CTFramesetterRef framesetter;
+@property (readwrite, nonatomic, assign) CTFramesetterRef highlightFramesetter;
+@property (readwrite, nonatomic, strong) NSDataDetector *dataDetector;
+@property (readwrite, nonatomic, strong) NSArray *links;
+@property (readwrite, nonatomic, strong) NSTextCheckingResult *activeLink;
+
+- (void)commonInit;
+- (void)setNeedsFramesetter;
+- (NSArray *)detectedLinksInString:(NSString *)string range:(NSRange)range error:(NSError **)error;
+- (NSTextCheckingResult *)linkAtCharacterIndex:(CFIndex)idx;
+- (NSTextCheckingResult *)linkAtPoint:(CGPoint)p;
+- (CFIndex)characterIndexAtPoint:(CGPoint)p;
+- (void)drawFramesetter:(CTFramesetterRef)framesetter textRange:(CFRange)textRange inRect:(CGRect)rect context:(CGContextRef)c;
+- (void)drawStrike:(CTFrameRef)frame inRect:(CGRect)rect context:(CGContextRef)c;
+@end
+
+@implementation TTTAttributedLabel {
+@private
+ BOOL _needsFramesetter;
+}
+
+@dynamic text;
+@synthesize attributedText = _attributedText;
+@synthesize inactiveAttributedText = _inactiveAttributedText;
+@synthesize renderedAttributedText = _renderedAttributedText;
+@synthesize framesetter = _framesetter;
+@synthesize highlightFramesetter = _highlightFramesetter;
+@synthesize delegate = _delegate;
+@synthesize dataDetectorTypes = _dataDetectorTypes;
+@synthesize dataDetector = _dataDetector;
+@synthesize links = _links;
+@synthesize linkAttributes = _linkAttributes;
+@synthesize activeLinkAttributes = _activeLinkAttributes;
+@synthesize shadowRadius = _shadowRadius;
+@synthesize leading = _leading;
+@synthesize lineHeightMultiple = _lineHeightMultiple;
+@synthesize firstLineIndent = _firstLineIndent;
+@synthesize textInsets = _textInsets;
+@synthesize verticalAlignment = _verticalAlignment;
+@synthesize activeLink = _activeLink;
+
+- (id)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+ if (!self) {
+ return nil;
+ }
+
+ [self commonInit];
+
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)coder {
+ self = [super initWithCoder:coder];
+ if (!self) {
+ return nil;
+ }
+
+ [self commonInit];
+
+ return self;
+}
+
+- (void)commonInit {
+ 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];
+
+ NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary];
+ [mutableActiveLinkAttributes setValue:(id)[[UIColor redColor] CGColor] forKey:(NSString*)kCTForegroundColorAttributeName];
+ [mutableActiveLinkAttributes setValue:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
+
+ self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes];
+
+ self.textInsets = UIEdgeInsetsZero;
+
+ self.userInteractionEnabled = YES;
+ self.multipleTouchEnabled = NO;
+}
+
+- (void)dealloc {
+ if (_framesetter) CFRelease(_framesetter);
+ if (_highlightFramesetter) CFRelease(_highlightFramesetter);
+}
+
+#pragma mark -
+
+- (void)setAttributedText:(NSAttributedString *)text {
+ if ([text isEqualToAttributedString:_attributedText]) {
+ return;
+ }
+
+ [self willChangeValueForKey:@"attributedText"];
+ _attributedText = [text copy];
+ [self didChangeValueForKey:@"attributedText"];
+
+ [self setNeedsFramesetter];
+}
+
+- (void)setNeedsFramesetter {
+ // Reset the rendered attributed text so it has a chance to regenerate
+ self.renderedAttributedText = nil;
+
+ _needsFramesetter = YES;
+}
+
+- (CTFramesetterRef)framesetter {
+ if (_needsFramesetter) {
+ @synchronized(self) {
+ if (_framesetter) CFRelease(_framesetter);
+ if (_highlightFramesetter) CFRelease(_highlightFramesetter);
+
+ self.framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText);
+ self.highlightFramesetter = nil;
+ _needsFramesetter = NO;
+ }
+ }
+
+ return _framesetter;
+}
+
+- (NSAttributedString *)renderedAttributedText {
+ if (!_renderedAttributedText) {
+ self.renderedAttributedText = NSAttributedStringBySettingColorFromContext(self.attributedText, self.textColor);
+ }
+
+ return _renderedAttributedText;
+}
+
+#pragma mark -
+
+- (void)setLinkActive:(BOOL)active withTextCheckingResult:(NSTextCheckingResult *)result {
+ if (result && [self.activeLinkAttributes count] > 0) {
+ if (active) {
+ if (!self.inactiveAttributedText) {
+ self.inactiveAttributedText = self.attributedText;
+ }
+
+ NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy];
+ [mutableAttributedString addAttributes:self.activeLinkAttributes range:result.range];
+ self.attributedText = mutableAttributedString;
+
+ [self setNeedsDisplay];
+ } else {
+ if (self.inactiveAttributedText) {
+ self.attributedText = self.inactiveAttributedText;
+ self.inactiveAttributedText = nil;
+
+ [self setNeedsDisplay];
+ }
+ }
+ }
+}
+
+#pragma mark -
+
+- (void)setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
+ [self willChangeValueForKey:@"dataDetectorTypes"];
+ _dataDetectorTypes = dataDetectorTypes;
+ [self didChangeValueForKey:@"dataDetectorTypes"];
+
+ if (self.dataDetectorTypes != UIDataDetectorTypeNone) {
+ self.dataDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeFromUIDataDetectorType(self.dataDetectorTypes) error:nil];
+ }
+}
+
+- (NSArray *)detectedLinksInString:(NSString *)string range:(NSRange)range error:(NSError **)error {
+ if (!string || !self.dataDetector) {
+ return [NSArray array];
+ }
+
+ return [self.dataDetector matchesInString:string options:0 range:range];
+}
+
+- (void)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes {
+ self.links = [self.links arrayByAddingObject:result];
+
+ if (attributes) {
+ NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
+ [mutableAttributedString addAttributes:attributes range:result.range];
+ self.attributedText = mutableAttributedString;
+ }
+}
+
+- (void)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result {
+ [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes];
+}
+
+- (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 ((CFIndex)range.location <= idx && idx <= (CFIndex)(range.location + range.length - 1)) {
+ return result;
+ }
+ }
+
+ return nil;
+}
+
+- (NSTextCheckingResult *)linkAtPoint:(CGPoint)p {
+ CFIndex idx = [self characterIndexAtPoint:p];
+ return [self linkAtCharacterIndex:idx];
+}
+
+- (CFIndex)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;
+ }
+
+ // Offset tap coordinates by textRect origin to make them relative to the origin of frame
+ p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
+ // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
+ p = CGPointMake(p.x, textRect.size.height - p.y);
+
+ CGMutablePathRef path = CGPathCreateMutable();
+ CGPathAddRect(path, NULL, textRect);
+ CTFrameRef frame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, [self.attributedText length]), path, NULL);
+ if (frame == NULL) {
+ CFRelease(path);
+ return NSNotFound;
+ }
+
+ CFArrayRef lines = CTFrameGetLines(frame);
+ NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
+ if (numberOfLines == 0) {
+ CFRelease(frame);
+ CFRelease(path);
+ return NSNotFound;
+ }
+
+ NSUInteger idx = NSNotFound;
+
+ CGPoint lineOrigins[numberOfLines];
+ CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
+
+ for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
+ CGPoint lineOrigin = lineOrigins[lineIndex];
+ CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
+
+ // Get bounding information of line
+ CGFloat ascent, descent, leading, width;
+ width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
+ CGFloat yMin = floor(lineOrigin.y - descent);
+ CGFloat yMax = ceil(lineOrigin.y + ascent);
+
+ // Check if we've already passed the line
+ if (p.y > yMax) {
+ break;
+ }
+ // Check if the point is within this line vertically
+ if (p.y >= yMin) {
+ // Check if the point is within this line horizontally
+ if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
+ // Convert CT coordinates to line-relative coordinates
+ CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
+ idx = CTLineGetStringIndexForPosition(line, relativePoint);
+ break;
+ }
+ }
+ }
+
+ 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);
+
+ [self drawBackground:frame inRect:rect context:c];
+
+ CFArrayRef lines = CTFrameGetLines(frame);
+ NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
+ BOOL truncateLastLine = (self.lineBreakMode == UILineBreakModeHeadTruncation || self.lineBreakMode == UILineBreakModeMiddleTruncation || self.lineBreakMode == UILineBreakModeTailTruncation);
+
+ CGPoint lineOrigins[numberOfLines];
+ CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
+
+ for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
+ CGPoint lineOrigin = lineOrigins[lineIndex];
+ CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
+ CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
+
+ if (lineIndex == numberOfLines - 1 && truncateLastLine) {
+ // Check if the range of text in the last line reaches the end of the full attributed string
+ CFRange lastLineRange = CTLineGetStringRange(line);
+
+ if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
+ // Get correct truncationType and attribute position
+ CTLineTruncationType truncationType;
+ NSUInteger truncationAttributePosition = lastLineRange.location;
+ UILineBreakMode lineBreakMode = self.lineBreakMode;
+
+ // Multiple lines, only use UILineBreakModeTailTruncation
+ if (numberOfLines != 1) {
+ lineBreakMode = UILineBreakModeTailTruncation;
+ }
+
+ switch (lineBreakMode) {
+ case UILineBreakModeHeadTruncation:
+ truncationType = kCTLineTruncationStart;
+ break;
+ case UILineBreakModeMiddleTruncation:
+ truncationType = kCTLineTruncationMiddle;
+ truncationAttributePosition += (lastLineRange.length / 2);
+ break;
+ case UILineBreakModeTailTruncation:
+ default:
+ truncationType = kCTLineTruncationEnd;
+ truncationAttributePosition += (lastLineRange.length - 1);
+ break;
+ }
+
+ // Get the attributes and use them to create the truncation token string
+ NSDictionary *tokenAttributes = [self.renderedAttributedText attributesAtIndex:truncationAttributePosition effectiveRange:NULL];
+ // \u2026 is the Unicode horizontal ellipsis character code
+ NSAttributedString *tokenString = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:tokenAttributes];
+ CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)tokenString);
+
+ // Append truncationToken to the string
+ // because if string isn't too long, CT wont add the truncationToken on it's own
+ // There is no change of a double truncationToken because CT only add the token if it removes characters (and the one we add will go first)
+ NSMutableAttributedString *truncationString = [[self.renderedAttributedText attributedSubstringFromRange:NSMakeRange(lastLineRange.location, lastLineRange.length)] mutableCopy];
+ if (lastLineRange.length > 0) {
+ // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the second would be on the next line.
+ unichar lastCharacter = [[truncationString string] characterAtIndex:lastLineRange.length - 1];
+ if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
+ [truncationString deleteCharactersInRange:NSMakeRange(lastLineRange.length - 1, 1)];
+ }
+ }
+ [truncationString appendAttributedString:tokenString];
+ CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
+
+ // Truncate the line in case it is too long.
+ CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
+ if (!truncatedLine) {
+ // If the line is not as wide as the truncationToken, truncatedLine is NULL
+ truncatedLine = CFRetain(truncationToken);
+ }
+
+ CTLineDraw(truncatedLine, c);
+
+ CFRelease(truncatedLine);
+ CFRelease(truncationLine);
+ CFRelease(truncationToken);
+ } else {
+ CTLineDraw(line, c);
+ }
+ } else {
+ CTLineDraw(line, c);
+ }
+ }
+
+ [self drawStrike:frame inRect:rect context:c];
+
+ CFRelease(frame);
+ CFRelease(path);
+}
+
+- (void)drawBackground:(CTFrameRef)frame inRect:(CGRect)rect context:(CGContextRef)c {
+ NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
+ CGPoint origins[[lines count]];
+ CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
+
+ CFIndex lineIndex = 0;
+ for (id line in lines) {
+ CGRect lineBounds = CTLineGetImageBounds((__bridge CTLineRef)line, c);
+ lineBounds.origin.x = origins[lineIndex].x;
+ lineBounds.origin.y = origins[lineIndex].y;
+
+ for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
+ NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef) glyphRun);
+ CGColorRef strokeColor = (__bridge CGColorRef)[attributes objectForKey:kTTTBackgroundStrokeColorAttributeName];
+ CGColorRef fillColor = (__bridge CGColorRef)[attributes objectForKey:kTTTBackgroundFillColorAttributeName];
+ CGFloat cornerRadius = [[attributes objectForKey:kTTTBackgroundCornerRadiusAttributeName] floatValue];
+ CGFloat lineWidth = [[attributes objectForKey:kTTTBackgroundLineWidthAttributeName] floatValue];
+
+ if (strokeColor || fillColor) {
+ CGRect runBounds = CGRectZero;
+ CGFloat ascent = 0.0f;
+ CGFloat descent = 0.0f;
+
+ runBounds.size.width = CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &ascent, &descent, NULL);
+ runBounds.size.height = ascent + descent;
+
+ CGFloat xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, CTRunGetStringRange((__bridge CTRunRef)glyphRun).location, NULL);
+ runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset;
+ runBounds.origin.y = origins[lineIndex].y + rect.origin.y;
+ runBounds.origin.y -= descent;
+
+ // Don't draw higlightedLinkBackground too far to the right
+ if (CGRectGetWidth(runBounds) > CGRectGetWidth(lineBounds)) {
+ runBounds.size.width = CGRectGetWidth(lineBounds);
+ }
+
+ CGRect rect = CGRectInset(CGRectInset(runBounds, -1.0f, -3.0f), lineWidth, lineWidth);
+ CGPathRef path = [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius] CGPath];
+
+ CGContextSetLineJoin(c, kCGLineJoinRound);
+
+ if (fillColor) {
+ CGContextSetFillColorWithColor(c, fillColor);
+ CGContextAddPath(c, path);
+ CGContextFillPath(c);
+ }
+
+ if (strokeColor) {
+ CGContextSetStrokeColorWithColor(c, strokeColor);
+ CGContextAddPath(c, path);
+ CGContextStrokePath(c);
+ }
+ }
+ }
+
+ lineIndex++;
+ }
+}
+
+- (void)drawStrike:(CTFrameRef)frame inRect:(CGRect)rect context:(CGContextRef)c {
+ NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
+ CGPoint origins[[lines count]];
+ CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
+
+ CFIndex lineIndex = 0;
+ for (id line in lines) {
+ CGRect lineBounds = CTLineGetImageBounds((__bridge CTLineRef)line, c);
+ lineBounds.origin.x = origins[lineIndex].x;
+ lineBounds.origin.y = origins[lineIndex].y;
+
+ for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
+ NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef) glyphRun);
+ BOOL strikeOut = [[attributes objectForKey:kTTTStrikeOutAttributeName] boolValue];
+ NSInteger superscriptStyle = [[attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
+
+ if (strikeOut) {
+ CGRect runBounds = CGRectZero;
+ CGFloat ascent = 0.0f;
+ CGFloat descent = 0.0f;
+
+ runBounds.size.width = CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &ascent, &descent, NULL);
+ runBounds.size.height = ascent + descent;
+
+ CGFloat xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, CTRunGetStringRange((__bridge CTRunRef)glyphRun).location, NULL);
+ runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset;
+ runBounds.origin.y = origins[lineIndex].y + rect.origin.y;
+ runBounds.origin.y -= descent;
+
+ // Don't draw strikeout too far to the right
+ if (CGRectGetWidth(runBounds) > CGRectGetWidth(lineBounds)) {
+ runBounds.size.width = CGRectGetWidth(lineBounds);
+ }
+
+ switch (superscriptStyle) {
+ case 1:
+ runBounds.origin.y -= ascent * 0.47f;
+ break;
+ case -1:
+ runBounds.origin.y += ascent * 0.25f;
+ break;
+ default:
+ break;
+ }
+
+ // Use text color, or default to black
+ id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
+
+ if (color) {
+ CGContextSetStrokeColorWithColor(c, (__bridge CGColorRef)color);
+ } else {
+ CGContextSetGrayStrokeColor(c, 0.0f, 1.0);
+ }
+
+ CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
+ CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font));
+ CGFloat y = roundf(runBounds.origin.y + runBounds.size.height / 2.0f);
+ CGContextMoveToPoint(c, runBounds.origin.x, y);
+ CGContextAddLineToPoint(c, runBounds.origin.x + runBounds.size.width, y);
+
+ CGContextStrokePath(c);
+ }
+ }
+
+ lineIndex++;
+ }
+}
+
+#pragma mark - TTTAttributedLabel
+
+- (void)setText:(id)text {
+ if ([text isKindOfClass:[NSString class]]) {
+ [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil];
+ return;
+ }
+
+ 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 {
+ NSMutableAttributedString *mutableAttributedString = nil;
+ if ([text isKindOfClass:[NSString class]]) {
+ mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:NSAttributedStringAttributesFromLabel(self)];
+ } else {
+ mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text];
+ [mutableAttributedString addAttributes:NSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])];
+ }
+
+ if (block) {
+ mutableAttributedString = block(mutableAttributedString);
+ }
+
+ [self setText:mutableAttributedString];
+}
+
+#pragma mark - UILabel
+
+- (void)setHighlighted:(BOOL)highlighted {
+ [super setHighlighted:highlighted];
+ [self setNeedsDisplay];
+}
+
+// Fixes crash when loading from a UIStoryboard
+- (UIColor *)textColor {
+ UIColor *color = [super textColor];
+ if (!color) {
+ color = [UIColor blackColor];
+ }
+
+ return color;
+}
+
+- (void)setTextColor:(UIColor *)textColor {
+ UIColor *oldTextColor = self.textColor;
+ [super setTextColor:textColor];
+
+ // Redraw to allow any ColorFromContext attributes a chance to update
+ if (textColor != oldTextColor) {
+ [self setNeedsFramesetter];
+ [self setNeedsDisplay];
+ }
+}
+
+- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {
+ if (!self.attributedText) {
+ return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
+ }
+
+ CGRect textRect = bounds;
+
+ // Adjust the text to be in the center vertically, if the text size is smaller than bounds
+ CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0, [self.attributedText length]), NULL, bounds.size, NULL);
+ textSize = CGSizeMake(ceilf(textSize.width), ceilf(textSize.height)); // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
+
+ if (textSize.height < textRect.size.height) {
+ CGFloat heightChange = (textRect.size.height - textSize.height);
+ CGFloat yOffset = 0.0f;
+ switch (self.verticalAlignment) {
+ case TTTAttributedLabelVerticalAlignmentTop:
+ heightChange = 0.0f;
+ break;
+ case TTTAttributedLabelVerticalAlignmentCenter:
+ yOffset = floorf((textRect.size.height - textSize.height) / 2.0f);
+ break;
+ case TTTAttributedLabelVerticalAlignmentBottom:
+ yOffset = textRect.size.height - textSize.height;
+ break;
+ }
+
+ textRect.origin.y += yOffset;
+ textRect.size = CGSizeMake(textRect.size.width, textRect.size.height - heightChange + yOffset);
+ }
+
+ return textRect;
+}
+
+- (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];
+ 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, rect.size.height);
+ CGContextScaleCTM(c, 1.0f, -1.0f);
+
+ CFRange textRange = CFRangeMake(0, [self.attributedText length]);
+
+ // First, get the text rect (which takes vertical centering into account)
+ CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines];
+
+ // CoreText draws it's text aligned to the bottom, so we move the CTM here to take our vertical offsets into account
+ CGContextTranslateCTM(c, 0.0f, rect.size.height - textRect.origin.y - textRect.size.height);
+
+ // 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.renderedAttributedText mutableCopy];
+ [mutableAttributedString addAttribute:(NSString *)kCTForegroundColorAttributeName value:(id)[self.highlightedTextColor CGColor] range:NSMakeRange(0, mutableAttributedString.length)];
+ self.highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge 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 - UIView
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ if (!self.attributedText) {
+ return [super sizeThatFits:size];
+ }
+
+ CFRange rangeToSize = CFRangeMake(0, [self.attributedText length]);
+ CGSize constraints = CGSizeMake(size.width, CGFLOAT_MAX);
+
+ if (self.numberOfLines == 1) {
+ // If there is one line, the size that fits is the full width of the line
+ constraints = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
+ } else if (self.numberOfLines > 0) {
+ // If the line count of the label more than 1, limit the range to size to the number of lines that have been set
+ CGMutablePathRef path = CGPathCreateMutable();
+ CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, CGFLOAT_MAX));
+ CTFrameRef frame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, 0), path, NULL);
+ CFArrayRef lines = CTFrameGetLines(frame);
+
+ if (CFArrayGetCount(lines) > 0) {
+ NSInteger lastVisibleLineIndex = MIN(self.numberOfLines, CFArrayGetCount(lines)) - 1;
+ CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
+
+ CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
+ rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
+ }
+
+ CFRelease(frame);
+ CFRelease(path);
+ }
+
+ CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, rangeToSize, NULL, constraints, NULL);
+
+ return CGSizeMake(ceilf(suggestedSize.width), ceilf(suggestedSize.height));
+}
+
+#pragma mark - UIResponder
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ UITouch *touch = [touches anyObject];
+
+ self.activeLink = [self linkAtPoint:[touch locationInView:self]];
+
+ if (self.activeLink) {
+ [self setLinkActive:YES withTextCheckingResult:self.activeLink];
+ } else {
+ [super touchesBegan:touches withEvent:event];
+ }
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+ if (self.activeLink) {
+ UITouch *touch = [touches anyObject];
+
+ if (self.activeLink != [self linkAtPoint:[touch locationInView:self]]) {
+ [self setLinkActive:NO withTextCheckingResult:self.activeLink];
+ } else {
+ [self setLinkActive:YES withTextCheckingResult:self.activeLink];
+ }
+ } else {
+ [super touchesMoved:touches withEvent:event];
+ }
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+ if (self.activeLink) {
+ UITouch *touch = [touches anyObject];
+ if (self.activeLink == [self linkAtPoint:[touch locationInView:self]]) {
+ [self setLinkActive:NO withTextCheckingResult:self.activeLink];
+
+ if (!self.delegate) {
+ return;
+ }
+
+ NSTextCheckingResult *result = self.activeLink;
+ switch (result.resultType) {
+ case NSTextCheckingTypeLink:
+ if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) {
+ [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL];
+ return;
+ }
+ break;
+ case NSTextCheckingTypeAddress:
+ if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) {
+ [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents];
+ return;
+ }
+ break;
+ case NSTextCheckingTypePhoneNumber:
+ if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) {
+ [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber];
+ return;
+ }
+ 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];
+ return;
+ } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) {
+ [self.delegate attributedLabel:self didSelectLinkWithDate:result.date];
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Fallback to `attributedLabel:didSelectLinkWithTextCheckingResult:` if no other delegate method matched.
+ if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTextCheckingResult:)]) {
+ [self.delegate attributedLabel:self didSelectLinkWithTextCheckingResult:result];
+ }
+ }
+ } else {
+ [super touchesEnded:touches withEvent:event];
+ }
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+ if (self.activeLink) {
+ [self setLinkActive:NO withTextCheckingResult:self.activeLink];
+ } else {
+ [super touchesCancelled:touches withEvent:event];
+ }
+}
+
+@end
+
+#pragma clang diagnostic pop