diff --git a/Backpack.podspec b/Backpack.podspec index 5be8011dc..a6f31855c 100644 --- a/Backpack.podspec +++ b/Backpack.podspec @@ -37,7 +37,6 @@ Pod::Spec.new do |s| s.public_header_files = 'Backpack/Backpack.h', 'Backpack/*/Classes/**/*.h' s.dependency 'FSCalendar', '~> 2.8.2' - s.dependency 'TTTAttributedLabel', '~> 2.0.0' s.dependency 'FloatingPanel', '2.5.3' s.dependency 'Backpack-Common' s.dependency 'MBProgressHUD', '~> 1.2.0' diff --git a/Backpack/TappableLinkLabel/Classes/BPKTappableLinkLabel.m b/Backpack/TappableLinkLabel/Classes/BPKTappableLinkLabel.m index 733f63557..c19ad42b8 100644 --- a/Backpack/TappableLinkLabel/Classes/BPKTappableLinkLabel.m +++ b/Backpack/TappableLinkLabel/Classes/BPKTappableLinkLabel.m @@ -17,7 +17,7 @@ */ #import "BPKTappableLinkLabel.h" -#import +#import "TTTAttributedLabel.h" #import #import diff --git a/Backpack/TappableLinkLabel/Classes/TTTAttributedLabel/TTTAttributedLabel.h b/Backpack/TappableLinkLabel/Classes/TTTAttributedLabel/TTTAttributedLabel.h new file mode 100644 index 000000000..8d1c53545 --- /dev/null +++ b/Backpack/TappableLinkLabel/Classes/TTTAttributedLabel/TTTAttributedLabel.h @@ -0,0 +1,707 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2018 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * 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 +#import + +//! Project version number for TTTAttributedLabel. +FOUNDATION_EXPORT double TTTAttributedLabelVersionNumber; + +//! Project version string for TTTAttributedLabel. +FOUNDATION_EXPORT const unsigned char TTTAttributedLabelVersionString[]; + +@class TTTAttributedLabelLink; + +/** + Vertical alignment for text in a label whose bounds are larger than its text bounds + */ +typedef NS_ENUM(NSInteger, TTTAttributedLabelVerticalAlignment) { + TTTAttributedLabelVerticalAlignmentCenter = 0, + TTTAttributedLabelVerticalAlignmentTop = 1, + TTTAttributedLabelVerticalAlignmentBottom = 2, +}; + +/** + 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 padding for the background fill. Value must be a `UIEdgeInsets`. Default value is `UIEdgeInsetsZero` (no padding). + */ +extern NSString *const kTTTBackgroundFillPaddingAttributeName; + +/** + 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 +@property(nonatomic, copy) IBInspectable id text; +@end + +IB_DESIGNABLE + +/** + `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` 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) + - `attributedText` - Do not set this property directly. Instead, pass an `NSAttributedString` to `text`. + - `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. + - `baselineAdjustment` - This property has no affect. + - `textAlignment` - This property does not support justified alignment. + - `NSTextAttachment` - This string attribute is not supported. + + Any properties affecting text or paragraph styling, such as `firstLineIndent` will only apply when text is set with an `NSString`. If the text is set + with an `NSAttributedString`, these properties will not apply. + + ### NSCoding + + `TTTAttributedLabel`, like `UILabel`, conforms to `NSCoding`. However, if the build target is set to less than iOS 6.0, `linkAttributes` and + `activeLinkAttributes` will not be encoded or decoded. This is due to an runtime exception thrown when attempting to copy non-object CoreText values + in dictionaries. + + @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. + + @bug Setting `attributedText` directly is not recommended, as it may cause a crash when attempting to access any links previously set. Instead, call + `setText:`, passing an `NSAttributedString`. + */ +@interface TTTAttributedLabel : UILabel + +/** + * The designated initializers are @c initWithFrame: and @c initWithCoder:. + * init will not properly initialize many required properties and other configuration. + */ +- (instancetype)init NS_UNAVAILABLE; + +///----------------------------- +/// @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) IBOutlet id delegate; + +///-------------------------------------------- +/// @name Detecting, Accessing, & Styling Links +///-------------------------------------------- + +/** + A bitmask of `NSTextCheckingType` which are used to automatically detect links in the label text. + + @warning You must specify `enabledTextCheckingTypes` before setting the `text`, with either `setText:` or + `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`. + */ +@property(nonatomic, assign) NSTextCheckingTypes enabledTextCheckingTypes; + +/** + 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 default `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 default `NSAttributedString` attributes to be applied to links when they are in the active state. If `nil` or an empty + `NSDictionary`, active links will not be styled. The default active link style is red and underlined. + */ +@property(nonatomic, strong) NSDictionary *activeLinkAttributes; + +/** + A dictionary containing the default `NSAttributedString` attributes to be applied to links when they are in the inactive state, which is triggered by + a change in `tintColor` in iOS 7 and later. If `nil` or an empty `NSDictionary`, inactive links will not be styled. The default inactive link style + is gray and unadorned. + */ +@property(nonatomic, strong) NSDictionary *inactiveLinkAttributes; + +/** + The edge inset for the background of a link. The default value is `{0, -1, 0, -1}`. + */ +@property(nonatomic, assign) UIEdgeInsets linkBackgroundEdgeInset; + +/** + Indicates if links will be detected within an extended area around the touch + to emulate the link detection behaviour of WKWebView. + Default value is NO. Enabling this may adversely impact performance. + */ +@property(nonatomic, assign) BOOL extendsLinkTouchArea; + +///--------------------------------------- +/// @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) IBInspectable CGFloat shadowRadius; + +/** + The shadow blur radius for the label when the label's `highlighted` property is `YES`. 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) IBInspectable CGFloat highlightedShadowRadius; +/** + The shadow offset for the label when the label's `highlighted` property is `YES`. A size of {0, 0} indicates no offset, with positive values + extending down and to the right. The default size is {0, 0}. + */ +@property(nonatomic, assign) IBInspectable CGSize highlightedShadowOffset; +/** + The shadow color for the label when the label's `highlighted` property is `YES`. The default value is `nil` (no shadow color). + */ +@property(nonatomic, strong) IBInspectable UIColor *highlightedShadowColor; + +/** + The amount to kern the next character. Default is standard kerning. If this attribute is set to 0.0, no kerning is done at all. + */ +@property(nonatomic, assign) IBInspectable CGFloat kern; + +///-------------------------------------------- +/// @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. + This applies to the full text, rather than any specific paragraph metrics. + */ +@property(nonatomic, assign) IBInspectable 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) IBInspectable CGFloat lineSpacing; + +/** + The minimum line height within the paragraph. If the value is 0.0, the minimum line height is set to the line height of the `font`. 0.0 by default. + */ +@property(nonatomic, assign) IBInspectable CGFloat minimumLineHeight; + +/** + The maximum line height within the paragraph. If the value is 0.0, the maximum line height is set to the line height of the `font`. 0.0 by default. + */ +@property(nonatomic, assign) IBInspectable CGFloat maximumLineHeight; + +/** + The line height multiple. This value is 1.0 by default. + */ +@property(nonatomic, assign) IBInspectable CGFloat lineHeightMultiple; + +/** + The distance, in points, from the margin to the text container. This value is `UIEdgeInsetsZero` by default. + sizeThatFits: will have its returned size increased by these margins. + drawTextInRect: will inset all drawn text by these margins. + */ +@property(nonatomic, assign) IBInspectable 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 Accessing Truncation Token Appearance +///-------------------------------------------- + +/** + The attributed string to apply to the truncation token at the end of a truncated line. + */ +@property(nonatomic, strong) IBInspectable NSAttributedString *attributedTruncationToken; + +///-------------------------- +/// @name Long press gestures +///-------------------------- + +/** + * The long-press gesture recognizer used internally by the label. + */ +@property(nonatomic, strong, readonly) UILongPressGestureRecognizer *longPressGestureRecognizer; + +///-------------------------------------------- +/// @name Calculating Size of Attributed String +///-------------------------------------------- + +/** + Calculate and return the size that best fits an attributed string, given the specified constraints on size and number of lines. + + @param attributedString The attributed string. + @param size The maximum dimensions used to calculate size. + @param numberOfLines The maximum number of lines in the text to draw, if the constraining size cannot accomodate the full attributed string. + + @return The size that fits the attributed string within the specified constraints. + */ ++ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString + withConstraints:(CGSize)size + limitedToNumberOfLines:(NSUInteger)numberOfLines; + +///---------------------------------- +/// @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. + + @warning Do not set this property directly. Instead, set @c text to an @c NSAttributedString. + */ +@property(readwrite, nonatomic, copy) NSAttributedString *attributedText; + +///------------------- +/// @name Adding Links +///------------------- + +/** + Adds a link. You can customize an individual link's appearance and accessibility value by creating your own @c TTTAttributedLabelLink and passing it + to this method. The other methods for adding links will use the label's default attributes. + + @warning Modifying the link's attribute dictionaries must be done before calling this method. + + @param link A @c TTTAttributedLabelLink object. + */ +- (void)addLink:(TTTAttributedLabelLink *)link; + +/** + Adds a link to an @c NSTextCheckingResult. + + @param result An @c NSTextCheckingResult representing the link's location and type. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result; + +/** + Adds a link to an @c NSTextCheckingResult. + + @param result An @c 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 set, the label's @c activeAttributes and @c + inactiveAttributes will be applied to the link. If `nil`, no attributes are added to the link. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)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. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)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." + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)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. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)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. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)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. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range; + +/** + Adds a link to transit information for a specified range in the label text. + + @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and + `NSTextCheckingFlightKey`. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @return The newly added link object. + */ +- (TTTAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components withRange:(NSRange)range; + +/** + Returns whether an @c NSTextCheckingResult is found at the give point. + + @discussion This can be used together with @c UITapGestureRecognizer to tap interactions with overlapping views. + + @param point The point inside the label. + */ +- (BOOL)containslinkAtPoint:(CGPoint)point; + +/** + Returns the @c TTTAttributedLabelLink at the give point if it exists. + + @discussion This can be used together with @c UIViewControllerPreviewingDelegate to peek into links. + + @param point The point inside the label. + */ +- (TTTAttributedLabelLink *)linkAtPoint:(CGPoint)point; + +@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 + +///----------------------------------- +/// @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 transit information + + @param label The label whose link was selected. + @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and + `NSTextCheckingFlightKey`. + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithTransitInformation:(NSDictionary *)components; + +/** + 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; + +///--------------------------------- +/// @name Responding to Long Presses +///--------------------------------- + +/** + * Long-press delegate methods include the CGPoint tapped within the label's coordinate space. + * This may be useful on iPad to present a popover from a specific origin point. + */ + +/** + Tells the delegate that the user long-pressed a link to a URL. + + @param label The label whose link was long pressed. + @param url The URL for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didLongPressLinkWithURL:(NSURL *)url atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to an address. + + @param label The label whose link was long pressed. + @param addressComponents The components of the address for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didLongPressLinkWithAddress:(NSDictionary *)addressComponents atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to a phone number. + + @param label The label whose link was long pressed. + @param phoneNumber The phone number for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didLongPressLinkWithPhoneNumber:(NSString *)phoneNumber atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to a date. + + @param label The label whose link was long pressed. + @param date The date for the selected link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didLongPressLinkWithDate:(NSDate *)date atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to a date with a time zone and duration. + + @param label The label whose link was long pressed. + @param date The date for the link. + @param timeZone The time zone of the date for the link. + @param duration The duration, in seconds from the date for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label + didLongPressLinkWithDate:(NSDate *)date + timeZone:(NSTimeZone *)timeZone + duration:(NSTimeInterval)duration + atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to transit information. + + @param label The label whose link was long pressed. + @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and + `NSTextCheckingFlightKey`. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didLongPressLinkWithTransitInformation:(NSDictionary *)components atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to a text checking result. + + @discussion Similar to `-attributedLabel:didSelectLinkWithTextCheckingResult:`, this method is called if a link is long pressed and the delegate does + not implement the method corresponding to this type of link. + + @param label The label whose link was long pressed. + @param result The custom text checking result. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(TTTAttributedLabel *)label didLongPressLinkWithTextCheckingResult:(NSTextCheckingResult *)result atPoint:(CGPoint)point; + +@end + +@interface TTTAttributedLabelLink : NSObject + +typedef void (^TTTAttributedLabelLinkBlock)(TTTAttributedLabel *, TTTAttributedLabelLink *); + +/** + An `NSTextCheckingResult` representing the link's location and type. + */ +@property(readonly, nonatomic, strong) NSTextCheckingResult *result; + +/** + A dictionary containing the @c NSAttributedString attributes to be applied to the link. + */ +@property(readonly, nonatomic, copy) NSDictionary *attributes; + +/** + A dictionary containing the @c NSAttributedString attributes to be applied to the link when it is in the active state. + */ +@property(readonly, nonatomic, copy) NSDictionary *activeAttributes; + +/** + A dictionary containing the @c NSAttributedString attributes to be applied to the link when it is in the inactive state, which is triggered by a + change in `tintColor` in iOS 7 and later. + */ +@property(readonly, nonatomic, copy) NSDictionary *inactiveAttributes; + +/** + Additional information about a link for VoiceOver users. Has default values if the link's @c result is @c NSTextCheckingTypeLink, @c + NSTextCheckingTypePhoneNumber, or @c NSTextCheckingTypeDate. + */ +@property(nonatomic, copy) NSString *accessibilityValue; + +/** + A block called when this link is tapped. + If non-nil, tapping on this link will call this block instead of the + @c TTTAttributedLabelDelegate tap methods, which will not be called for this link. + */ +@property(nonatomic, copy) TTTAttributedLabelLinkBlock linkTapBlock; + +/** + A block called when this link is long-pressed. + If non-nil, long pressing on this link will call this block instead of the + @c TTTAttributedLabelDelegate long press methods, which will not be called for this link. + */ +@property(nonatomic, copy) TTTAttributedLabelLinkBlock linkLongPressBlock; + +/** + Initializes a link using the attribute dictionaries specified. + + @param attributes The @c attributes property for the link. + @param activeAttributes The @c activeAttributes property for the link. + @param inactiveAttributes The @c inactiveAttributes property for the link. + @param result An @c NSTextCheckingResult representing the link's location and type. + + @return The initialized link object. + */ +- (instancetype)initWithAttributes:(NSDictionary *)attributes + activeAttributes:(NSDictionary *)activeAttributes + inactiveAttributes:(NSDictionary *)inactiveAttributes + textCheckingResult:(NSTextCheckingResult *)result; + +/** + Initializes a link using the attribute dictionaries set on a specified label. + + @param label The attributed label from which to inherit attribute dictionaries. + @param result An @c NSTextCheckingResult representing the link's location and type. + + @return The initialized link object. + */ +- (instancetype)initWithAttributesFromLabel:(TTTAttributedLabel *)label textCheckingResult:(NSTextCheckingResult *)result; + +@end diff --git a/Backpack/TappableLinkLabel/Classes/TTTAttributedLabel/TTTAttributedLabel.m b/Backpack/TappableLinkLabel/Classes/TTTAttributedLabel/TTTAttributedLabel.m new file mode 100644 index 000000000..3fe3785f9 --- /dev/null +++ b/Backpack/TappableLinkLabel/Classes/TTTAttributedLabel/TTTAttributedLabel.m @@ -0,0 +1,1855 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2018 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * 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" + +#import +#import +#import + +#define kTTTLineBreakWordWrapTextWidthScalingFactor (M_PI / M_E) + +static CGFloat const TTTFLOAT_MAX = 100000; + +NSString *const kTTTStrikeOutAttributeName = @"TTTStrikeOutAttribute"; +NSString *const kTTTBackgroundFillColorAttributeName = @"TTTBackgroundFillColor"; +NSString *const kTTTBackgroundFillPaddingAttributeName = @"TTTBackgroundFillPadding"; +NSString *const kTTTBackgroundStrokeColorAttributeName = @"TTTBackgroundStrokeColor"; +NSString *const kTTTBackgroundLineWidthAttributeName = @"TTTBackgroundLineWidth"; +NSString *const kTTTBackgroundCornerRadiusAttributeName = @"TTTBackgroundCornerRadius"; + +const NSTextAlignment TTTTextAlignmentLeft = NSTextAlignmentLeft; +const NSTextAlignment TTTTextAlignmentCenter = NSTextAlignmentCenter; +const NSTextAlignment TTTTextAlignmentRight = NSTextAlignmentRight; +const NSTextAlignment TTTTextAlignmentJustified = NSTextAlignmentJustified; +const NSTextAlignment TTTTextAlignmentNatural = NSTextAlignmentNatural; + +const NSLineBreakMode TTTLineBreakByWordWrapping = NSLineBreakByWordWrapping; +const NSLineBreakMode TTTLineBreakByCharWrapping = NSLineBreakByCharWrapping; +const NSLineBreakMode TTTLineBreakByClipping = NSLineBreakByClipping; +const NSLineBreakMode TTTLineBreakByTruncatingHead = NSLineBreakByTruncatingHead; +const NSLineBreakMode TTTLineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle; +const NSLineBreakMode TTTLineBreakByTruncatingTail = NSLineBreakByTruncatingTail; + +typedef NSTextAlignment TTTTextAlignment; +typedef NSLineBreakMode TTTLineBreakMode; + +static inline CGFLOAT_TYPE CGFloat_ceil(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return ceil(cgfloat); +#else + return ceilf(cgfloat); +#endif +} + +static inline CGFLOAT_TYPE CGFloat_floor(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return floor(cgfloat); +#else + return floorf(cgfloat); +#endif +} + +static inline CGFLOAT_TYPE CGFloat_round(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return round(cgfloat); +#else + return roundf(cgfloat); +#endif +} + +static inline CGFLOAT_TYPE CGFloat_sqrt(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return sqrt(cgfloat); +#else + return sqrtf(cgfloat); +#endif +} + +static inline CGFloat TTTFlushFactorForTextAlignment(NSTextAlignment textAlignment) { + switch (textAlignment) { + case TTTTextAlignmentCenter: + return 0.5f; + case TTTTextAlignmentRight: + return 1.0f; + case TTTTextAlignmentLeft: + default: + return 0.0f; + } +} + +static inline NSDictionary *NSAttributedStringAttributesFromLabel(TTTAttributedLabel *label) { + NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary]; + + [mutableAttributes setObject:label.font forKey:(NSString *)kCTFontAttributeName]; + [mutableAttributes setObject:label.textColor forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName]; + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = label.textAlignment; + paragraphStyle.lineSpacing = label.lineSpacing; + paragraphStyle.minimumLineHeight = label.minimumLineHeight > 0 ? label.minimumLineHeight : label.font.lineHeight * label.lineHeightMultiple; + paragraphStyle.maximumLineHeight = label.maximumLineHeight > 0 ? label.maximumLineHeight : label.font.lineHeight * label.lineHeightMultiple; + paragraphStyle.lineHeightMultiple = label.lineHeightMultiple; + paragraphStyle.firstLineHeadIndent = label.firstLineIndent; + + if (label.numberOfLines == 1) { + paragraphStyle.lineBreakMode = label.lineBreakMode; + } else { + paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; + } + + [mutableAttributes setObject:paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName]; + + return [NSDictionary dictionaryWithDictionary:mutableAttributes]; +} + +static inline CGColorRef CGColorRefFromColor(id color); +static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes); + +static inline NSAttributedString *NSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale) { + NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy]; + [mutableAttributedString + enumerateAttribute:(NSString *)kCTFontAttributeName + inRange:NSMakeRange(0, [mutableAttributedString length]) + options:0 + usingBlock:^(id value, NSRange range, BOOL *__unused stop) { + UIFont *font = (UIFont *)value; + if (font) { + NSString *fontName; + CGFloat pointSize; + + if ([font isKindOfClass:[UIFont class]]) { + fontName = font.fontName; + pointSize = font.pointSize; + } else { + fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey)); + pointSize = CTFontGetSize((__bridge CTFontRef)font); + } + + [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range]; + CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)fontName, CGFloat_floor(pointSize * scale), NULL); + [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range]; + CFRelease(fontRef); + } + }]; + + return mutableAttributedString; +} + +static inline NSAttributedString *NSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) { + if (!color) { + return attributedString; + } + + NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy]; + [mutableAttributedString + enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName + inRange:NSMakeRange(0, [mutableAttributedString length]) + options:0 + usingBlock:^(id value, NSRange range, __unused BOOL *stop) { + BOOL usesColorFromContext = (BOOL)value; + if (usesColorFromContext) { + [mutableAttributedString setAttributes:[NSDictionary dictionaryWithObject:color + forKey:(NSString *)kCTForegroundColorAttributeName] + range:range]; + [mutableAttributedString removeAttribute:(NSString *)kCTForegroundColorFromContextAttributeName range:range]; + } + }]; + + return mutableAttributedString; +} + +static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, + NSAttributedString *attributedString, CGSize size, + NSUInteger numberOfLines) { + CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]); + CGSize constraints = CGSizeMake(size.width, TTTFLOAT_MAX); + + if (numberOfLines == 1) { + // If there is one line, the size that fits is the full width of the line + constraints = CGSizeMake(TTTFLOAT_MAX, TTTFLOAT_MAX); + } else if (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, TTTFLOAT_MAX)); + CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL); + CFArrayRef lines = CTFrameGetLines(frame); + + if (CFArrayGetCount(lines) > 0) { + NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1; + CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex); + + CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine); + rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length); + } + + CFRelease(frame); + CGPathRelease(path); + } + + CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL); + + return CGSizeMake(CGFloat_ceil(suggestedSize.width), CGFloat_ceil(suggestedSize.height)); +} + +@interface TTTAccessibilityElement : UIAccessibilityElement +@property(nonatomic, weak) UIView *superview; +@property(nonatomic, assign) CGRect boundingRect; +@end + +@implementation TTTAccessibilityElement + +- (CGRect)accessibilityFrame { + return UIAccessibilityConvertFrameToScreenCoordinates(self.boundingRect, self.superview); +} + +@end + +@interface TTTAttributedLabel () +@property(readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText; +@property(readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText; +@property(readwrite, atomic, strong) NSDataDetector *dataDetector; +@property(readwrite, nonatomic, strong) NSArray *linkModels; +@property(readwrite, nonatomic, strong) TTTAttributedLabelLink *activeLink; +@property(readwrite, nonatomic, strong) NSArray *accessibilityElements; + +- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender; +@end + +@implementation TTTAttributedLabel { + @private + BOOL _needsFramesetter; + CTFramesetterRef _framesetter; + CTFramesetterRef _highlightFramesetter; +} + +@dynamic text; +@synthesize attributedText = _attributedText; + +#ifndef kCFCoreFoundationVersionNumber_iOS_7_0 +#define kCFCoreFoundationVersionNumber_iOS_7_0 847.2 +#endif + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0) { + Class class = [self class]; + Class superclass = class_getSuperclass(class); + + NSArray *strings = @[ + NSStringFromSelector(@selector(isAccessibilityElement)), + NSStringFromSelector(@selector(accessibilityElementCount)), + NSStringFromSelector(@selector(accessibilityElementAtIndex:)), + NSStringFromSelector(@selector(indexOfAccessibilityElement:)), + ]; + + for (NSString *string in strings) { + SEL selector = NSSelectorFromString(string); + IMP superImplementation = class_getMethodImplementation(superclass, selector); + Method method = class_getInstanceMethod(class, selector); + const char *types = method_getTypeEncoding(method); + class_replaceMethod(class, selector, superImplementation, types); + } + } + }); +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { + return nil; + } + + [self commonInit]; + + return self; +} + +- (void)commonInit { + self.userInteractionEnabled = YES; +#if !TARGET_OS_TV + self.multipleTouchEnabled = NO; +#endif + + self.textInsets = UIEdgeInsetsZero; + self.lineHeightMultiple = 1.0f; + + self.linkModels = [NSArray array]; + + self.linkBackgroundEdgeInset = UIEdgeInsetsMake(0.0f, -1.0f, 0.0f, -1.0f); + + NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary]; + [mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + + NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary]; + [mutableActiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + + NSMutableDictionary *mutableInactiveLinkAttributes = [NSMutableDictionary dictionary]; + [mutableInactiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + + if ([NSMutableParagraphStyle class]) { + [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableActiveLinkAttributes setObject:[UIColor redColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableInactiveLinkAttributes setObject:[UIColor grayColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + } else { + [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableActiveLinkAttributes setObject:(__bridge id)[[UIColor redColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableInactiveLinkAttributes setObject:(__bridge id)[[UIColor grayColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + } + + self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes]; + self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes]; + self.inactiveLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableInactiveLinkAttributes]; + _extendsLinkTouchArea = NO; + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureDidFire:)]; + self.longPressGestureRecognizer.delegate = self; + [self addGestureRecognizer:self.longPressGestureRecognizer]; +} + +- (void)dealloc { + if (_framesetter) { + CFRelease(_framesetter); + } + + if (_highlightFramesetter) { + CFRelease(_highlightFramesetter); + } + + if (_longPressGestureRecognizer) { + [self removeGestureRecognizer:_longPressGestureRecognizer]; + } +} + +#pragma mark - + ++ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString + withConstraints:(CGSize)size + limitedToNumberOfLines:(NSUInteger)numberOfLines { + if (!attributedString || attributedString.length == 0) { + return CGSizeZero; + } + + CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString); + + CGSize calculatedSize = CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, attributedString, size, numberOfLines); + + CFRelease(framesetter); + + return calculatedSize; +} + +#pragma mark - + +- (void)setAttributedText:(NSAttributedString *)text { + if ([text isEqualToAttributedString:_attributedText]) { + return; + } + + _attributedText = [text copy]; + + [self setNeedsFramesetter]; + [self setNeedsDisplay]; + + if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { + [self invalidateIntrinsicContentSize]; + } + + [super setText:[self.attributedText string]]; +} + +- (NSAttributedString *)renderedAttributedText { + if (!_renderedAttributedText) { + NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; + + if (self.attributedTruncationToken) { + [fullString appendAttributedString:self.attributedTruncationToken]; + } + + NSAttributedString *string = [[NSAttributedString alloc] initWithAttributedString:fullString]; + self.renderedAttributedText = NSAttributedStringBySettingColorFromContext(string, self.textColor); + } + + return _renderedAttributedText; +} + +- (NSArray *)links { + return [_linkModels valueForKey:@"result"]; +} + +- (void)setLinkModels:(NSArray *)linkModels { + _linkModels = linkModels; + + self.accessibilityElements = nil; +} + +- (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) { + CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText); + [self setFramesetter:framesetter]; + [self setHighlightFramesetter:nil]; + _needsFramesetter = NO; + + if (framesetter) { + CFRelease(framesetter); + } + } + } + + return _framesetter; +} + +- (void)setFramesetter:(CTFramesetterRef)framesetter { + if (framesetter) { + CFRetain(framesetter); + } + + if (_framesetter) { + CFRelease(_framesetter); + } + + _framesetter = framesetter; +} + +- (CTFramesetterRef)highlightFramesetter { + return _highlightFramesetter; +} + +- (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter { + if (highlightFramesetter) { + CFRetain(highlightFramesetter); + } + + if (_highlightFramesetter) { + CFRelease(_highlightFramesetter); + } + + _highlightFramesetter = highlightFramesetter; +} + +#pragma mark - + +- (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)enabledTextCheckingTypes { + if (self.enabledTextCheckingTypes == enabledTextCheckingTypes) { + return; + } + + _enabledTextCheckingTypes = enabledTextCheckingTypes; + + // one detector instance per type (combination), fast reuse e.g. in cells + static NSMutableDictionary *dataDetectorsByType = nil; + + if (!dataDetectorsByType) { + dataDetectorsByType = [NSMutableDictionary dictionary]; + } + + if (enabledTextCheckingTypes) { + if (![dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]) { + NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:enabledTextCheckingTypes error:nil]; + if (detector) { + [dataDetectorsByType setObject:detector forKey:@(enabledTextCheckingTypes)]; + } + } + self.dataDetector = [dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]; + } else { + self.dataDetector = nil; + } +} + +- (void)addLink:(TTTAttributedLabelLink *)link { + [self addLinks:@[link]]; +} + +- (void)addLinks:(NSArray *)links { + NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels]; + + NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy]; + + for (TTTAttributedLabelLink *link in links) { + if (link.attributes) { + [mutableAttributedString addAttributes:link.attributes range:link.result.range]; + } + } + + self.attributedText = mutableAttributedString; + [self setNeedsDisplay]; + + [mutableLinkModels addObjectsFromArray:links]; + + self.linkModels = [NSArray arrayWithArray:mutableLinkModels]; +} + +- (TTTAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result attributes:(NSDictionary *)attributes { + return [self addLinksWithTextCheckingResults:@[result] attributes:attributes].firstObject; +} + +- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results attributes:(NSDictionary *)attributes { + NSMutableArray *links = [NSMutableArray array]; + + for (NSTextCheckingResult *result in results) { + NSDictionary *activeAttributes = attributes ? self.activeLinkAttributes : nil; + NSDictionary *inactiveAttributes = attributes ? self.inactiveLinkAttributes : nil; + + TTTAttributedLabelLink *link = [[TTTAttributedLabelLink alloc] initWithAttributes:attributes + activeAttributes:activeAttributes + inactiveAttributes:inactiveAttributes + textCheckingResult:result]; + + [links addObject:link]; + } + + [self addLinks:links]; + + return links; +} + +- (TTTAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result { + return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes]; +} + +- (TTTAttributedLabelLink *)addLinkToURL:(NSURL *)url withRange:(NSRange)range { + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]]; +} + +- (TTTAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range { + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]]; +} + +- (TTTAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range { + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]]; +} + +- (TTTAttributedLabelLink *)addLinkToDate:(NSDate *)date withRange:(NSRange)range { + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]]; +} + +- (TTTAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range { + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range + date:date + timeZone:timeZone + duration:duration]]; +} + +- (TTTAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components withRange:(NSRange)range { + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult transitInformationCheckingResultWithRange:range components:components]]; +} + +#pragma mark - + +- (BOOL)containslinkAtPoint:(CGPoint)point { + return [self linkAtPoint:point] != nil; +} + +- (TTTAttributedLabelLink *)linkAtPoint:(CGPoint)point { + + // Stop quickly if none of the points to be tested are in the bounds. + if (!CGRectContainsPoint(CGRectInset(self.bounds, -15.f, -15.f), point) || self.links.count == 0) { + return nil; + } + + TTTAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]]; + + if (!result && self.extendsLinkTouchArea) { + result = [self linkAtRadius:2.5f aroundPoint:point] + ?: [self linkAtRadius:5.f aroundPoint:point] + ?: [self linkAtRadius:7.5f aroundPoint:point] + ?: [self linkAtRadius:12.5f aroundPoint:point] + ?: [self linkAtRadius:15.f aroundPoint:point]; + } + + return result; +} + +- (TTTAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point { + const CGFloat diagonal = CGFloat_sqrt(2 * radius * radius); + const CGPoint deltas[] = { + CGPointMake(0, -radius), CGPointMake(0, radius), // Above and below + CGPointMake(-radius, 0), CGPointMake(radius, 0), // Beside + CGPointMake(-diagonal, -diagonal), CGPointMake(-diagonal, diagonal), + CGPointMake(diagonal, diagonal), CGPointMake(diagonal, -diagonal) // Diagonal + }; + const size_t count = sizeof(deltas) / sizeof(CGPoint); + + TTTAttributedLabelLink *link = nil; + + for (NSUInteger i = 0; i < count && link.result == nil; i++) { + CGPoint currentPoint = CGPointMake(point.x + deltas[i].x, point.y + deltas[i].y); + link = [self linkAtCharacterIndex:[self characterIndexAtPoint:currentPoint]]; + } + + return link; +} + +- (TTTAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx { + // Do not enumerate if the index is outside of the bounds of the text. + if (!NSLocationInRange((NSUInteger)idx, NSMakeRange(0, self.attributedText.length))) { + return nil; + } + + NSEnumerator *enumerator = [self.linkModels reverseObjectEnumerator]; + TTTAttributedLabelLink *link = nil; + while ((link = [enumerator nextObject])) { + if (NSLocationInRange((NSUInteger)idx, link.result.range)) { + return link; + } + } + + return nil; +} + +- (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, (CFIndex)[self.attributedText length]), path, NULL); + if (frame == NULL) { + CGPathRelease(path); + return NSNotFound; + } + + CFArrayRef lines = CTFrameGetLines(frame); + NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines); + if (numberOfLines == 0) { + CFRelease(frame); + CGPathRelease(path); + return NSNotFound; + } + + CFIndex 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 = 0.0f, descent = 0.0f, leading = 0.0f; + CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading); + CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent); + CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent); + + // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter + CGFloat flushFactor = TTTFlushFactorForTextAlignment(self.textAlignment); + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width); + lineOrigin.x = penOffset; + + // 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); + CGPathRelease(path); + + return idx; +} + +- (CGRect)boundingRectForCharacterRange:(NSRange)range { + NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:mutableAttributedString]; + + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + [textStorage addLayoutManager:layoutManager]; + + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size]; + [layoutManager addTextContainer:textContainer]; + + NSRange glyphRange; + [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange]; + + return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; +} + +- (void)drawFramesetter:(CTFramesetterRef)framesetter + attributedString:(NSAttributedString *)attributedString + 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 == TTTLineBreakByTruncatingHead || self.lineBreakMode == TTTLineBreakByTruncatingMiddle || + self.lineBreakMode == TTTLineBreakByTruncatingTail); + + 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); + + CGFloat descent = 0.0f; + CTLineGetTypographicBounds((CTLineRef)line, NULL, &descent, NULL); + + // Adjust pen offset for flush depending on text alignment + CGFloat flushFactor = TTTFlushFactorForTextAlignment(self.textAlignment); + + 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; + CFIndex truncationAttributePosition = lastLineRange.location; + TTTLineBreakMode lineBreakMode = self.lineBreakMode; + + // Multiple lines, only use UILineBreakModeTailTruncation + if (numberOfLines != 1) { + lineBreakMode = TTTLineBreakByTruncatingTail; + } + + switch (lineBreakMode) { + case TTTLineBreakByTruncatingHead: + truncationType = kCTLineTruncationStart; + break; + case TTTLineBreakByTruncatingMiddle: + truncationType = kCTLineTruncationMiddle; + truncationAttributePosition += (lastLineRange.length / 2); + break; + case TTTLineBreakByTruncatingTail: + default: + truncationType = kCTLineTruncationEnd; + truncationAttributePosition += (lastLineRange.length - 1); + break; + } + + NSAttributedString *attributedTruncationString = self.attributedTruncationToken; + if (!attributedTruncationString) { + NSString *truncationTokenString = @"\u2026"; // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026) + + NSDictionary *truncationTokenStringAttributes = truncationTokenStringAttributes = + [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL]; + + attributedTruncationString = [[NSAttributedString alloc] initWithString:truncationTokenString + attributes:truncationTokenStringAttributes]; + } + CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTruncationString); + + // Append truncationToken to the string + // because if string isn't too long, CT won't add the truncationToken on its own. + // There is no chance of a double truncationToken because CT only adds the + // token if it removes characters (and the one we add will go first) + NSMutableAttributedString *truncationString = [[NSMutableAttributedString alloc] + initWithAttributedString:[attributedString attributedSubstringFromRange:NSMakeRange((NSUInteger)lastLineRange.location, + (NSUInteger)lastLineRange.length)]]; + 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:(NSUInteger)(lastLineRange.length - 1)]; + if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) { + [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)]; + } + } + [truncationString appendAttributedString:attributedTruncationString]; + 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); + } + + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width); + CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender); + + CTLineDraw(truncatedLine, c); + + NSRange linkRange; + if ([attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange]) { + NSRange tokenRange = [truncationString.string rangeOfString:attributedTruncationString.string]; + NSRange tokenLinkRange = + NSMakeRange((NSUInteger)(lastLineRange.location + lastLineRange.length) - tokenRange.length, (NSUInteger)tokenRange.length); + + [self addLinkToURL:[attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange] + withRange:tokenLinkRange]; + } + + CFRelease(truncatedLine); + CFRelease(truncationLine); + CFRelease(truncationToken); + } else { + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width); + CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender); + CTLineDraw(line, c); + } + } else { + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width); + CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender); + CTLineDraw(line, c); + } + } + + [self drawStrike:frame inRect:rect context:c]; + + CFRelease(frame); + CGPathRelease(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) { + CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f; + CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading); + + for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) { + NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef)glyphRun); + CGColorRef strokeColor = CGColorRefFromColor([attributes objectForKey:kTTTBackgroundStrokeColorAttributeName]); + CGColorRef fillColor = CGColorRefFromColor([attributes objectForKey:kTTTBackgroundFillColorAttributeName]); + UIEdgeInsets fillPadding = [[attributes objectForKey:kTTTBackgroundFillPaddingAttributeName] UIEdgeInsetsValue]; + CGFloat cornerRadius = [[attributes objectForKey:kTTTBackgroundCornerRadiusAttributeName] floatValue]; + CGFloat lineWidth = [[attributes objectForKey:kTTTBackgroundLineWidthAttributeName] floatValue]; + + if (strokeColor || fillColor) { + CGRect runBounds = CGRectZero; + CGFloat runAscent = 0.0f; + CGFloat runDescent = 0.0f; + + runBounds.size.width = + (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL) + + fillPadding.left + fillPadding.right; + runBounds.size.height = runAscent + runDescent + fillPadding.top + fillPadding.bottom; + + CGFloat xOffset = 0.0f; + CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun); + switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) { + case kCTRunStatusRightToLeft: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL); + break; + default: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL); + break; + } + + runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset - fillPadding.left - rect.origin.x; + runBounds.origin.y = origins[lineIndex].y + rect.origin.y - fillPadding.bottom - rect.origin.y; + runBounds.origin.y -= runDescent; + + // Don't draw higlightedLinkBackground too far to the right + if (CGRectGetWidth(runBounds) > width) { + runBounds.size.width = width; + } + + CGPathRef path = [[UIBezierPath + bezierPathWithRoundedRect:CGRectInset(UIEdgeInsetsInsetRect(runBounds, self.linkBackgroundEdgeInset), lineWidth, lineWidth) + 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:(__unused 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) { + CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f; + CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading); + + 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 runAscent = 0.0f; + CGFloat runDescent = 0.0f; + + runBounds.size.width = + (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL); + runBounds.size.height = runAscent + runDescent; + + CGFloat xOffset = 0.0f; + CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun); + switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) { + case kCTRunStatusRightToLeft: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL); + break; + default: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL); + break; + } + runBounds.origin.x = origins[lineIndex].x + xOffset; + runBounds.origin.y = origins[lineIndex].y; + runBounds.origin.y -= runDescent; + + // Don't draw strikeout too far to the right + if (CGRectGetWidth(runBounds) > width) { + runBounds.size.width = width; + } + + switch (superscriptStyle) { + case 1: + runBounds.origin.y -= runAscent * 0.47f; + break; + case -1: + runBounds.origin.y += runAscent * 0.25f; + break; + default: + break; + } + + // Use text color, or default to black + id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName]; + if (color) { + CGContextSetStrokeColorWithColor(c, CGColorRefFromColor(color)); + } else { + CGContextSetGrayStrokeColor(c, 0.0f, 1.0); + } + + CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL); + CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font)); + CFRelease(font); + + CGFloat y = CGFloat_round(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 { + NSParameterAssert(!text || [text isKindOfClass:[NSAttributedString class]] || [text isKindOfClass:[NSString class]]); + + if ([text isKindOfClass:[NSString class]]) { + [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil]; + return; + } + + self.attributedText = text; + self.activeLink = nil; + + self.linkModels = [NSArray array]; + if (text && self.attributedText && self.enabledTextCheckingTypes) { + __weak __typeof(self) weakSelf = self; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + + NSDataDetector *dataDetector = strongSelf.dataDetector; + if (dataDetector && [dataDetector respondsToSelector:@selector(matchesInString:options:range:)]) { + NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string] + options:0 + range:NSMakeRange(0, [(NSAttributedString *)text length])]; + if ([results count] > 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[strongSelf.attributedText string] isEqualToString:[(NSAttributedString *)text string]]) { + [strongSelf addLinksWithTextCheckingResults:results attributes:strongSelf.linkAttributes]; + } + }); + } + } + }); + } + + [self.attributedText enumerateAttribute:NSLinkAttributeName + inRange:NSMakeRange(0, self.attributedText.length) + options:0 + usingBlock:^(id value, __unused NSRange range, __unused BOOL *stop) { + if (value) { + NSURL *URL = [value isKindOfClass:[NSString class]] ? [NSURL URLWithString:value] : value; + [self addLinkToURL:URL withRange:range]; + } + }]; +} + +- (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]; +} + +- (void)setActiveLink:(TTTAttributedLabelLink *)activeLink { + _activeLink = activeLink; + + NSDictionary *activeAttributes = activeLink.activeAttributes ?: self.activeLinkAttributes; + + if (_activeLink && activeAttributes.count > 0) { + if (!self.inactiveAttributedText) { + self.inactiveAttributedText = [self.attributedText copy]; + } + + NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy]; + if (self.activeLink.result.range.length > 0 && + NSLocationInRange(NSMaxRange(self.activeLink.result.range) - 1, NSMakeRange(0, [self.inactiveAttributedText length]))) { + [mutableAttributedString addAttributes:activeAttributes range:self.activeLink.result.range]; + } + + self.attributedText = mutableAttributedString; + [self setNeedsDisplay]; + + [CATransaction flush]; + } else if (self.inactiveAttributedText) { + self.attributedText = self.inactiveAttributedText; + self.inactiveAttributedText = nil; + + [self setNeedsDisplay]; + } +} + +- (void)setLinkAttributes:(NSDictionary *)linkAttributes { + _linkAttributes = convertNSAttributedStringAttributesToCTAttributes(linkAttributes); +} + +- (void)setActiveLinkAttributes:(NSDictionary *)activeLinkAttributes { + _activeLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(activeLinkAttributes); +} + +- (void)setInactiveLinkAttributes:(NSDictionary *)inactiveLinkAttributes { + _inactiveLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(inactiveLinkAttributes); +} + +#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 { + bounds = UIEdgeInsetsInsetRect(bounds, self.textInsets); + if (!self.attributedText) { + return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines]; + } + + CGRect textRect = bounds; + + // Calculate height with a minimum of double the font pointSize, to ensure that CTFramesetterSuggestFrameSizeWithConstraints doesn't return + // CGSizeZero, as it would if textRect height is insufficient. + textRect.size.height = MAX(self.font.lineHeight * MAX(2, numberOfLines), bounds.size.height); + + // Adjust the text to be in the center vertically, if the text size is smaller than bounds + CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), NULL, + textRect.size, NULL); + textSize = + CGSizeMake(CGFloat_ceil(textSize.width), + CGFloat_ceil(textSize.height)); // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes + + if (textSize.height < bounds.size.height) { + CGFloat yOffset = 0.0f; + switch (self.verticalAlignment) { + case TTTAttributedLabelVerticalAlignmentCenter: + yOffset = CGFloat_floor((bounds.size.height - textSize.height) / 2.0f); + break; + case TTTAttributedLabelVerticalAlignmentBottom: + yOffset = bounds.size.height - textSize.height; + break; + case TTTAttributedLabelVerticalAlignmentTop: + default: + break; + } + + textRect.origin.y += yOffset; + } + + return textRect; +} + +- (void)drawTextInRect:(CGRect)rect { + CGRect insetRect = UIEdgeInsetsInsetRect(rect, self.textInsets); + if (!self.attributedText) { + [super drawTextInRect:insetRect]; + return; + } + + NSAttributedString *originalAttributedText = nil; + + // Adjust the font size to fit width, if necessarry + if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) { + // Framesetter could still be working with a resized version of the text; + // need to reset so we start from the original font size. + // See #393. + [self setNeedsFramesetter]; + [self setNeedsDisplay]; + + if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { + [self invalidateIntrinsicContentSize]; + } + + // Use infinite width to find the max width, which will be compared to availableWidth if needed. + CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(TTTFLOAT_MAX, TTTFLOAT_MAX) : CGSizeZero; + + CGFloat textWidth = [self sizeThatFits:maxSize].width; + CGFloat availableWidth = self.frame.size.width * self.numberOfLines; + if (self.numberOfLines > 1 && self.lineBreakMode == TTTLineBreakByWordWrapping) { + textWidth *= kTTTLineBreakWordWrapTextWidthScalingFactor; + } + + if (textWidth > availableWidth && textWidth > 0.0f) { + originalAttributedText = [self.attributedText copy]; + + CGFloat scaleFactor = availableWidth / textWidth; + if ([self respondsToSelector:@selector(minimumScaleFactor)] && self.minimumScaleFactor > scaleFactor) { + scaleFactor = self.minimumScaleFactor; + } + + self.attributedText = NSAttributedStringByScalingFontSize(self.attributedText, scaleFactor); + } + } + + CGContextRef c = UIGraphicsGetCurrentContext(); + CGContextSaveGState(c); + { + 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, insetRect.size.height); + CGContextScaleCTM(c, 1.0f, -1.0f); + + CFRange textRange = CFRangeMake(0, (CFIndex)[self.attributedText length]); + + // First, get the text rect (which takes vertical centering into account) + CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines]; + + // CoreText draws its text aligned to the bottom, so we move the CTM here to take our vertical offsets into account + CGContextTranslateCTM(c, insetRect.origin.x, insetRect.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]); + } else if (self.highlightedShadowColor) { + CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, [self.highlightedShadowColor CGColor]); + } + + // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one) + if (self.highlightedTextColor && self.highlighted) { + NSMutableAttributedString *highlightAttributedString = [self.renderedAttributedText mutableCopy]; + [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName + value:(id)[self.highlightedTextColor CGColor] + range:NSMakeRange(0, highlightAttributedString.length)]; + + if (![self highlightFramesetter]) { + CTFramesetterRef highlightFramesetter = + CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString); + [self setHighlightFramesetter:highlightFramesetter]; + CFRelease(highlightFramesetter); + } + + [self drawFramesetter:[self highlightFramesetter] + attributedString:highlightAttributedString + textRange:textRange + inRect:textRect + context:c]; + } else { + [self drawFramesetter:[self framesetter] attributedString:self.renderedAttributedText textRange:textRange inRect:textRect context:c]; + } + + // If we adjusted the font size, set it back to its original size + if (originalAttributedText) { + // Use ivar directly to avoid clearing out framesetter and renderedAttributedText + _attributedText = originalAttributedText; + } + } + CGContextRestoreGState(c); +} + +#pragma mark - UIAccessibilityElement + +- (BOOL)isAccessibilityElement { + return NO; +} + +- (NSInteger)accessibilityElementCount { + return (NSInteger)[[self accessibilityElements] count]; +} + +- (id)accessibilityElementAtIndex:(NSInteger)index { + return [[self accessibilityElements] objectAtIndex:(NSUInteger)index]; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element { + return (NSInteger)[[self accessibilityElements] indexOfObject:element]; +} + +- (NSArray *)accessibilityElements { + if (!_accessibilityElements) { + @synchronized(self) { + NSMutableArray *mutableAccessibilityItems = [NSMutableArray array]; + + for (TTTAttributedLabelLink *link in self.linkModels) { + + if (link.result.range.location == NSNotFound) { + continue; + } + + NSString *sourceText = [self.text isKindOfClass:[NSString class]] ? self.text : [(NSAttributedString *)self.text string]; + + NSString *accessibilityLabel = [sourceText substringWithRange:link.result.range]; + NSString *accessibilityValue = link.accessibilityValue; + + if (accessibilityLabel) { + TTTAccessibilityElement *linkElement = [[TTTAccessibilityElement alloc] initWithAccessibilityContainer:self]; + linkElement.accessibilityTraits = UIAccessibilityTraitLink; + linkElement.boundingRect = [self boundingRectForCharacterRange:link.result.range]; + linkElement.superview = self; + linkElement.accessibilityLabel = accessibilityLabel; + + if (![accessibilityLabel isEqualToString:accessibilityValue]) { + linkElement.accessibilityValue = accessibilityValue; + } + + [mutableAccessibilityItems addObject:linkElement]; + } + } + + TTTAccessibilityElement *baseElement = [[TTTAccessibilityElement alloc] initWithAccessibilityContainer:self]; + baseElement.accessibilityLabel = [super accessibilityLabel]; + baseElement.accessibilityHint = [super accessibilityHint]; + baseElement.accessibilityValue = [super accessibilityValue]; + baseElement.boundingRect = self.bounds; + baseElement.superview = self; + baseElement.accessibilityTraits = [super accessibilityTraits]; + + [mutableAccessibilityItems addObject:baseElement]; + + self.accessibilityElements = [NSArray arrayWithArray:mutableAccessibilityItems]; + } + } + + return _accessibilityElements; +} + +#pragma mark - UIView + +- (CGSize)sizeThatFits:(CGSize)size { + if (!self.attributedText) { + return [super sizeThatFits:size]; + } else { + NSAttributedString *string = [self renderedAttributedText]; + + CGSize labelSize = + CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([self framesetter], string, size, (NSUInteger)self.numberOfLines); + labelSize.width += self.textInsets.left + self.textInsets.right; + labelSize.height += self.textInsets.top + self.textInsets.bottom; + + return labelSize; + } +} + +- (CGSize)intrinsicContentSize { + // There's an implicit width from the original UILabel implementation + return [self sizeThatFits:[super intrinsicContentSize]]; +} + +- (void)tintColorDidChange { + if (!self.inactiveLinkAttributes || [self.inactiveLinkAttributes count] == 0) { + return; + } + + BOOL isInactive = (self.tintAdjustmentMode == UIViewTintAdjustmentModeDimmed); + + NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy]; + for (TTTAttributedLabelLink *link in self.linkModels) { + NSDictionary *attributesToRemove = isInactive ? link.attributes : link.inactiveAttributes; + NSDictionary *attributesToAdd = isInactive ? link.inactiveAttributes : link.attributes; + + [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop) { + if (NSMaxRange(link.result.range) <= mutableAttributedString.length) { + [mutableAttributedString removeAttribute:name range:link.result.range]; + } + }]; + + if (attributesToAdd) { + if (NSMaxRange(link.result.range) <= mutableAttributedString.length) { + [mutableAttributedString addAttributes:attributesToAdd range:link.result.range]; + } + } + } + + self.attributedText = mutableAttributedString; + + [self setNeedsDisplay]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + if (![self linkAtPoint:point] || !self.userInteractionEnabled || self.hidden || self.alpha < 0.01) { + return [super hitTest:point withEvent:event]; + } + + return self; +} + +#pragma mark - UIResponder + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(__unused id)sender { +#if !TARGET_OS_TV + return (action == @selector(copy:)); +#else + return NO; +#endif +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + UITouch *touch = [touches anyObject]; + + self.activeLink = [self linkAtPoint:[touch locationInView:self]]; + + if (!self.activeLink) { + [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.activeLink = nil; + } + } else { + [super touchesMoved:touches withEvent:event]; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.activeLink) { + if (self.activeLink.linkTapBlock) { + self.activeLink.linkTapBlock(self, self.activeLink); + self.activeLink = nil; + return; + } + + NSTextCheckingResult *result = self.activeLink.result; + self.activeLink = nil; + + 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; + case NSTextCheckingTypeTransitInformation: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTransitInformation:)]) { + [self.delegate attributedLabel:self didSelectLinkWithTransitInformation:result.components]; + return; + } + 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.activeLink = nil; + } else { + [super touchesCancelled:touches withEvent:event]; + } +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + return [self containslinkAtPoint:[touch locationInView:self]]; +} + +#pragma mark - UILongPressGestureRecognizer + +- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender { + switch (sender.state) { + case UIGestureRecognizerStateBegan: { + CGPoint touchPoint = [sender locationInView:self]; + TTTAttributedLabelLink *link = [self linkAtPoint:touchPoint]; + + if (link) { + if (link.linkLongPressBlock) { + link.linkLongPressBlock(self, link); + return; + } + + NSTextCheckingResult *result = link.result; + + if (!result) { + return; + } + + switch (result.resultType) { + case NSTextCheckingTypeLink: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithURL:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithURL:result.URL atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypeAddress: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithAddress:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithAddress:result.addressComponents atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypePhoneNumber: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithPhoneNumber:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithPhoneNumber:result.phoneNumber atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypeDate: + if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel: + didLongPressLinkWithDate:timeZone:duration:atPoint:)]) { + [self.delegate attributedLabel:self + didLongPressLinkWithDate:result.date + timeZone:result.timeZone + duration:result.duration + atPoint:touchPoint]; + return; + } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypeTransitInformation: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTransitInformation:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithTransitInformation:result.components atPoint:touchPoint]; + return; + } + default: + break; + } + + // Fallback to `attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:` if no other delegate method matched. + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithTextCheckingResult:result atPoint:touchPoint]; + } + } + break; + } + default: + break; + } +} + +#if !TARGET_OS_TV +#pragma mark - UIResponderStandardEditActions + +- (void)copy:(__unused id)sender { + [[UIPasteboard generalPasteboard] setString:self.text]; +} +#endif + +#pragma mark - NSCoding + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:@(self.enabledTextCheckingTypes) forKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]; + + [coder encodeObject:self.linkModels forKey:NSStringFromSelector(@selector(linkModels))]; + if ([NSMutableParagraphStyle class]) { + [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))]; + [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))]; + [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]; + } + [coder encodeObject:@(self.shadowRadius) forKey:NSStringFromSelector(@selector(shadowRadius))]; + [coder encodeObject:@(self.highlightedShadowRadius) forKey:NSStringFromSelector(@selector(highlightedShadowRadius))]; + [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))]; + [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))]; + [coder encodeObject:@(self.kern) forKey:NSStringFromSelector(@selector(kern))]; + [coder encodeObject:@(self.firstLineIndent) forKey:NSStringFromSelector(@selector(firstLineIndent))]; + [coder encodeObject:@(self.lineSpacing) forKey:NSStringFromSelector(@selector(lineSpacing))]; + [coder encodeObject:@(self.lineHeightMultiple) forKey:NSStringFromSelector(@selector(lineHeightMultiple))]; + [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))]; + [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))]; + + [coder encodeObject:self.attributedTruncationToken forKey:NSStringFromSelector(@selector(attributedTruncationToken))]; + + [coder encodeObject:NSStringFromUIEdgeInsets(self.linkBackgroundEdgeInset) forKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]; + [coder encodeObject:self.attributedText forKey:NSStringFromSelector(@selector(attributedText))]; + [coder encodeObject:self.text forKey:NSStringFromSelector(@selector(text))]; +} + +- (id)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (!self) { + return nil; + } + + [self commonInit]; + + if ([coder containsValueForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]) { + self.enabledTextCheckingTypes = [[coder decodeObjectForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))] unsignedLongLongValue]; + } + + if ([NSMutableParagraphStyle class]) { + if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))]) { + self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))]) { + self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]) { + self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]; + } + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(links))]) { + NSArray *oldLinks = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))]; + [self addLinksWithTextCheckingResults:oldLinks attributes:nil]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(linkModels))]) { + self.linkModels = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkModels))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))]) { + self.shadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(shadowRadius))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))]) { + self.highlightedShadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowRadius))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]) { + self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))]) { + self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(kern))]) { + self.kern = [[coder decodeObjectForKey:NSStringFromSelector(@selector(kern))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))]) { + self.firstLineIndent = [[coder decodeObjectForKey:NSStringFromSelector(@selector(firstLineIndent))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(lineSpacing))]) { + self.lineSpacing = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineSpacing))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumLineHeight))]) { + self.minimumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(minimumLineHeight))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(maximumLineHeight))]) { + self.maximumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(maximumLineHeight))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))]) { + self.lineHeightMultiple = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineHeightMultiple))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))]) { + self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))]) { + self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedTruncationToken))]) { + self.attributedTruncationToken = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedTruncationToken))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]) { + self.linkBackgroundEdgeInset = UIEdgeInsetsFromString([coder decodeObjectForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]); + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedText))]) { + self.attributedText = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedText))]; + } else { + self.text = super.text; + } + + return self; +} + +@end + +#pragma mark - TTTAttributedLabelLink + +@implementation TTTAttributedLabelLink + +- (instancetype)initWithAttributes:(NSDictionary *)attributes + activeAttributes:(NSDictionary *)activeAttributes + inactiveAttributes:(NSDictionary *)inactiveAttributes + textCheckingResult:(NSTextCheckingResult *)result { + + if ((self = [super init])) { + _result = result; + _attributes = [attributes copy]; + _activeAttributes = [activeAttributes copy]; + _inactiveAttributes = [inactiveAttributes copy]; + } + + return self; +} + +- (instancetype)initWithAttributesFromLabel:(TTTAttributedLabel *)label textCheckingResult:(NSTextCheckingResult *)result { + + return [self initWithAttributes:label.linkAttributes + activeAttributes:label.activeLinkAttributes + inactiveAttributes:label.inactiveLinkAttributes + textCheckingResult:result]; +} + +#pragma mark - Accessibility + +- (NSString *)accessibilityValue { + if ([_accessibilityValue length] == 0) { + switch (self.result.resultType) { + case NSTextCheckingTypeLink: + _accessibilityValue = self.result.URL.absoluteString; + break; + case NSTextCheckingTypePhoneNumber: + _accessibilityValue = self.result.phoneNumber; + break; + case NSTextCheckingTypeDate: + _accessibilityValue = [NSDateFormatter localizedStringFromDate:self.result.date + dateStyle:NSDateFormatterLongStyle + timeStyle:NSDateFormatterLongStyle]; + break; + default: + break; + } + } + + return _accessibilityValue; +} + +#pragma mark - NSCoding + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.result forKey:NSStringFromSelector(@selector(result))]; + [aCoder encodeObject:self.attributes forKey:NSStringFromSelector(@selector(attributes))]; + [aCoder encodeObject:self.activeAttributes forKey:NSStringFromSelector(@selector(activeAttributes))]; + [aCoder encodeObject:self.inactiveAttributes forKey:NSStringFromSelector(@selector(inactiveAttributes))]; + [aCoder encodeObject:self.accessibilityValue forKey:NSStringFromSelector(@selector(accessibilityValue))]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + if ((self = [super init])) { + _result = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(result))]; + _attributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(attributes))]; + _activeAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(activeAttributes))]; + _inactiveAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(inactiveAttributes))]; + self.accessibilityValue = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(accessibilityValue))]; + } + + return self; +} + +@end + +#pragma mark - + +static inline CGColorRef CGColorRefFromColor(id color) { + return [color isKindOfClass:[UIColor class]] ? [color CGColor] : (__bridge CGColorRef)color; +} + +static inline CTFontRef CTFontRefFromUIFont(UIFont *font) { + CTFontRef ctfont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL); + return CFAutorelease(ctfont); +} + +static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes) { + if (!attributes) + return nil; + + NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary]; + + NSDictionary *NSToCTAttributeNamesMap = @{ + NSFontAttributeName: (NSString *)kCTFontAttributeName, + NSBackgroundColorAttributeName: (NSString *)kTTTBackgroundFillColorAttributeName, + NSForegroundColorAttributeName: (NSString *)kCTForegroundColorAttributeName, + NSUnderlineColorAttributeName: (NSString *)kCTUnderlineColorAttributeName, + NSUnderlineStyleAttributeName: (NSString *)kCTUnderlineStyleAttributeName, + NSStrokeWidthAttributeName: (NSString *)kCTStrokeWidthAttributeName, + NSStrokeColorAttributeName: (NSString *)kCTStrokeWidthAttributeName, + NSKernAttributeName: (NSString *)kCTKernAttributeName, + NSLigatureAttributeName: (NSString *)kCTLigatureAttributeName + }; + + [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + key = [NSToCTAttributeNamesMap objectForKey:key] ?: key; + + if (![NSMutableParagraphStyle class]) { + if ([value isKindOfClass:[UIFont class]]) { + value = (__bridge id)CTFontRefFromUIFont(value); + } else if ([value isKindOfClass:[UIColor class]]) { + value = (__bridge id)((UIColor *)value).CGColor; + } + } + + [mutableAttributes setObject:value forKey:key]; + }]; + + return [NSDictionary dictionaryWithDictionary:mutableAttributes]; +} diff --git a/Example/Podfile.lock b/Example/Podfile.lock index ab905791e..6fffe7e7b 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -14,7 +14,6 @@ PODS: - FloatingPanel (= 2.5.3) - FSCalendar (~> 2.8.2) - MBProgressHUD (~> 1.2.0) - - TTTAttributedLabel (~> 2.0.0) - Backpack-Common (1.0) - Backpack-Common/Tests (1.0) - Backpack-Fonts (0.0.1) @@ -29,21 +28,18 @@ PODS: - FSCalendar (~> 2.8.2) - MBProgressHUD (~> 1.2.0) - SnapshotTesting (~> 1.9.0) - - TTTAttributedLabel (~> 2.0.0) - Backpack/UnitTests (1.0): - Backpack-Common - FloatingPanel (= 2.5.3) - FSCalendar (~> 2.8.2) - MBProgressHUD (~> 1.2.0) - OCMock (~> 3.8.1) - - TTTAttributedLabel (~> 2.0.0) - FloatingPanel (2.5.3) - FSCalendar (2.8.4) - MBProgressHUD (1.2.0) - OCMock (3.8.1) - SnapshotTesting (1.9.0) - SwiftLint (0.43.1) - - TTTAttributedLabel (2.0.0) DEPENDENCIES: - AppCenter (~> 4.2.0) @@ -67,7 +63,6 @@ SPEC REPOS: - OCMock - SnapshotTesting - SwiftLint - - TTTAttributedLabel EXTERNAL SOURCES: Backpack: @@ -81,7 +76,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppCenter: 87ef6eefd8ade4df59e88951288587429f3dd2a5 - Backpack: 6ea2b8690a1ac4e10a1bc19c84e9106acdfe2c02 + Backpack: 0ef7b6b61a6715c01ce50379e521912034bb8d66 Backpack-Common: 9a47d236de2f7fea7366f7a893423b8375ee7a39 Backpack-Fonts: 8d10ac600d738cb7ae6e49fee969800c2db51f6b Backpack-SwiftUI: f1d0e98dc0beb0f0a44178796aae6c407fb9d7d3 @@ -91,7 +86,6 @@ SPEC CHECKSUMS: OCMock: 29f6e52085b4e7d9b075cbf03ed7c3112f82f934 SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 - TTTAttributedLabel: 8cffe8e127e4e82ff3af1e5386d4cd0ad000b656 PODFILE CHECKSUM: db90d722d21004d77a9d6cf29da110b644239024