diff --git a/TTTAttributedLabel/Info.plist b/TTTAttributedLabel/Info.plist new file mode 100644 index 00000000..e1fe4cfb --- /dev/null +++ b/TTTAttributedLabel/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/TTTAttributedLabel/TTTAttributedLabel.h b/TTTAttributedLabel/TTTAttributedLabel.h new file mode 100644 index 00000000..8b9d34f7 --- /dev/null +++ b/TTTAttributedLabel/TTTAttributedLabel.h @@ -0,0 +1,777 @@ +// 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 UIWebView. + 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/TTTAttributedLabel/TTTAttributedLabel.m b/TTTAttributedLabel/TTTAttributedLabel.m new file mode 100644 index 00000000..c9b2f09e --- /dev/null +++ b/TTTAttributedLabel/TTTAttributedLabel.m @@ -0,0 +1,2147 @@ +// 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/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7c9770c3..b94336f3 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 04496BCC216252E5001F1B23 /* TTTAttributedLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = 04496BCA216252E5001F1B23 /* TTTAttributedLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */; }; + 04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 04496BD52162530A001F1B23 /* TTTAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */; }; + 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD621625361001F1B23 /* ContentLabel.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; @@ -65,7 +70,6 @@ D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; }; D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; }; D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; }; - D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A0CD22132153900640E3B /* HTMLContentLabel.swift */; }; D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; @@ -88,7 +92,6 @@ D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; }; D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; }; D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; }; - D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */; }; D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; }; D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5F22135BC260057A976 /* Conversation.storyboard */; }; D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationViewController.swift */; }; @@ -103,6 +106,7 @@ D6C693CD2161257B007D6A6D /* SilentActionPermissionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6C693CC2161257B007D6A6D /* SilentActionPermissionCell.xib */; }; D6C693CF216125FC007D6A6D /* SilentActionPermissionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693CE216125FC007D6A6D /* SilentActionPermissionTableViewCell.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; + D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */; }; D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; }; D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; }; D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; }; @@ -120,6 +124,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 04496BCD216252E5001F1B23 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 04496BC7216252E5001F1B23; + remoteInfo = TTTAttributedLabel; + }; D61099B52144B0CC00432DC2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; @@ -164,6 +175,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */, D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */, D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */, ); @@ -173,6 +185,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TTTAttributedLabel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 04496BCA216252E5001F1B23 /* TTTAttributedLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TTTAttributedLabel.h; sourceTree = ""; }; + 04496BCB216252E5001F1B23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TTTAttributedLabel.m; sourceTree = ""; }; + 04496BD621625361001F1B23 /* ContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabel.swift; sourceTree = ""; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; @@ -232,7 +249,6 @@ D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = ""; }; D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = ""; }; D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = ""; }; - D64A0CD22132153900640E3B /* HTMLContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLContentLabel.swift; sourceTree = ""; }; D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; @@ -254,7 +270,6 @@ D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = ""; }; D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = ""; }; D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = ""; }; - D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = ""; }; D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = ""; }; D667E5F22135BC260057A976 /* Conversation.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Conversation.storyboard; sourceTree = ""; }; D667E5F42135BCD50057A976 /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = ""; }; @@ -269,6 +284,7 @@ D6C693CC2161257B007D6A6D /* SilentActionPermissionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SilentActionPermissionCell.xib; sourceTree = ""; }; D6C693CE216125FC007D6A6D /* SilentActionPermissionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPermissionTableViewCell.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; + D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = ""; }; D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = ""; }; D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = ""; }; D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; @@ -292,6 +308,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 04496BC5216252E5001F1B23 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A82144B0CC00432DC2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -311,6 +334,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */, D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */, D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */, ); @@ -333,6 +357,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 04496BC9216252E5001F1B23 /* TTTAttributedLabel */ = { + isa = PBXGroup; + children = ( + 04496BCA216252E5001F1B23 /* TTTAttributedLabel.h */, + 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */, + 04496BCB216252E5001F1B23 /* Info.plist */, + ); + path = TTTAttributedLabel; + sourceTree = ""; + }; D61099AC2144B0CC00432DC2 /* Pachyderm */ = { isa = PBXGroup; children = ( @@ -606,8 +640,8 @@ D6BED1722126661300F02DA0 /* Views */ = { isa = PBXGroup; children = ( - D64A0CD22132153900640E3B /* HTMLContentLabel.swift */, - D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */, + 04496BD621625361001F1B23 /* ContentLabel.swift */, + D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */, D6C94D882139E6EC00CB5196 /* AttachmentView.swift */, D6333B762138D94E00CE884A /* ComposeMediaView.swift */, D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */, @@ -636,6 +670,7 @@ D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */, D61099AC2144B0CC00432DC2 /* Pachyderm */, D61099B92144B0CC00432DC2 /* PachydermTests */, + 04496BC9216252E5001F1B23 /* TTTAttributedLabel */, D6D4DDCE212518A000E1C4BB /* Tusker */, D6D4DDE3212518A200E1C4BB /* TuskerTests */, D6D4DDEE212518A200E1C4BB /* TuskerUITests */, @@ -652,6 +687,7 @@ D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */, D61099AB2144B0CC00432DC2 /* Pachyderm.framework */, D61099B32144B0CC00432DC2 /* PachydermTests.xctest */, + 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */, ); name = Products; sourceTree = ""; @@ -706,6 +742,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 04496BC3216252E5001F1B23 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 04496BCC216252E5001F1B23 /* TTTAttributedLabel.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A62144B0CC00432DC2 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -717,6 +761,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 04496BC7216252E5001F1B23 /* TTTAttributedLabel */ = { + isa = PBXNativeTarget; + buildConfigurationList = 04496BD3216252E5001F1B23 /* Build configuration list for PBXNativeTarget "TTTAttributedLabel" */; + buildPhases = ( + 04496BC3216252E5001F1B23 /* Headers */, + 04496BC4216252E5001F1B23 /* Sources */, + 04496BC5216252E5001F1B23 /* Frameworks */, + 04496BC6216252E5001F1B23 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TTTAttributedLabel; + productName = TTTAttributedLabel; + productReference = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */; + productType = "com.apple.product-type.framework"; + }; D61099AA2144B0CC00432DC2 /* Pachyderm */ = { isa = PBXNativeTarget; buildConfigurationList = D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */; @@ -767,6 +829,7 @@ ); dependencies = ( D61099BF2144B0CC00432DC2 /* PBXTargetDependency */, + 04496BCE216252E5001F1B23 /* PBXTargetDependency */, ); name = Tusker; productName = Tusker; @@ -819,6 +882,9 @@ LastUpgradeCheck = 1010; ORGANIZATIONNAME = Shadowfacts; TargetAttributes = { + 04496BC7216252E5001F1B23 = { + CreatedOnToolsVersion = 10.1; + }; D61099AA2144B0CC00432DC2 = { CreatedOnToolsVersion = 10.0; LastSwiftMigration = 1000; @@ -858,11 +924,19 @@ D6D4DDEA212518A200E1C4BB /* TuskerUITests */, D61099AA2144B0CC00432DC2 /* Pachyderm */, D61099B22144B0CC00432DC2 /* PachydermTests */, + 04496BC7216252E5001F1B23 /* TTTAttributedLabel */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 04496BC6216252E5001F1B23 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A92144B0CC00432DC2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -918,6 +992,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 04496BC4216252E5001F1B23 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04496BD52162530A001F1B23 /* TTTAttributedLabel.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D61099A72144B0CC00432DC2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -975,6 +1057,7 @@ files = ( D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, + D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, @@ -998,10 +1081,10 @@ D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D641C78F213DF2AA004B4513 /* VisibilityTableViewController.swift in Sources */, D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */, + 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D6C693CA2161253F007D6A6D /* SilentActionPermissionsTableViewController.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */, - D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */, D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, @@ -1014,7 +1097,6 @@ D641C77D213CB024004B4513 /* FollowNotificationTableViewCell.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */, - D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */, D6C693CF216125FC007D6A6D /* SilentActionPermissionTableViewCell.swift in Sources */, D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */, D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, @@ -1043,6 +1125,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 04496BCE216252E5001F1B23 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 04496BC7216252E5001F1B23 /* TTTAttributedLabel */; + targetProxy = 04496BCD216252E5001F1B23 /* PBXContainerItemProxy */; + }; D61099B62144B0CC00432DC2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D61099AA2144B0CC00432DC2 /* Pachyderm */; @@ -1090,6 +1177,62 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 04496BD1216252E5001F1B23 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TTTAttributedLabel/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.TTTAttributedLabel; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 04496BD2216252E5001F1B23 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TTTAttributedLabel/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.TTTAttributedLabel; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; D61099C32144B0CC00432DC2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1432,6 +1575,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 04496BD3216252E5001F1B23 /* Build configuration list for PBXNativeTarget "TTTAttributedLabel" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 04496BD1216252E5001F1B23 /* Debug */, + 04496BD2216252E5001F1B23 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Tusker/Views/ContentLabel.swift b/Tusker/Views/ContentLabel.swift new file mode 100644 index 00000000..404c5591 --- /dev/null +++ b/Tusker/Views/ContentLabel.swift @@ -0,0 +1,128 @@ +// +// ContentLabel.swift +// Tusker +// +// Created by Shadowfacts on 10/1/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import TTTAttributedLabel +import Pachyderm +import SwiftSoup + +protocol ContentLabelNavigationDelegate { + func selected(mention: Mention) + + func selected(tag: Hashtag) + + func selected(url: URL) +} + +class ContentLabel: TTTAttributedLabel { + + var navigationDelegate: ContentLabelNavigationDelegate? + + override func awakeFromNib() { + super.awakeFromNib() + + isUserInteractionEnabled = true + } + + // MARK: - HTML Parsing + func setTextFromHtml(_ html: String) { + let doc = try! SwiftSoup.parse(html) + let body = doc.body()! + + let (attributedText, links) = attributedTextForHTMLNode(body) + let mutAttrString = NSMutableAttributedString(attributedString: attributedText) + mutAttrString.trimCharactersInSet(.whitespacesAndNewlines) + mutAttrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutAttrString.length)) + self.text = mutAttrString + for (range, url) in links { + let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url) + let link = TTTAttributedLabelLink(attributes: [ + NSAttributedString.Key.foregroundColor: UIColor.blue + ], activeAttributes: [ + kTTTBackgroundFillColorAttributeName: UIColor(displayP3Red: 0.9, green: 0.9, blue: 0.9, alpha: 1), + kTTTBackgroundFillPaddingAttributeName: UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), + kTTTBackgroundCornerRadiusAttributeName: NSNumber(value: 5) + ], inactiveAttributes: [:], textCheckingResult: result)! + link.linkTapBlock = self.linkTapped + link.linkLongPressBlock = self.linkLongPressed + addLink(link) + } + + } + + private func attributedTextForHTMLNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) { + switch node { + case let node as TextNode: + return (NSAttributedString(string: node.text()), [:]) + case let node as Element: + var links = [NSRange: URL]() + let attributed = NSMutableAttributedString() + for child in node.getChildNodes() { + let (text, childLinks) = attributedTextForHTMLNode(child) + for (range, url) in childLinks { + let newRange = NSRange(location: range.location + attributed.length, length: range.length) + links[newRange] = url + } + attributed.append(text) + } + + switch node.tagName() { + case "br": + attributed.append(NSAttributedString(string: "\n")) + case "a": + if let link = try? node.attr("href"), + let url = URL(string: link) { + let linkRange = NSRange(location: 0, length: attributed.length) + links[linkRange] = url + } + case "p": + attributed.append(NSAttributedString(string: "\n\n")) + default: + break + } + + return (attributed, links) + default: + fatalError("Unexpected node type: \(type(of: node))") + } + } + + // MARK: - Interaction + func linkTapped(_ label: TTTAttributedLabel!, _ link: TTTAttributedLabelLink!) { + if let navigationDelegate = navigationDelegate, + let url = link.result.url { + let text = (self.text as! NSString).substring(with: link.result.range) + if let mention = getMention(for: url, text: text) { + navigationDelegate.selected(mention: mention) + } else if let tag = getHashtag(for: url, text: text) { + navigationDelegate.selected(tag: tag) + } else { + navigationDelegate.selected(url: url) + } + } + } + + func linkLongPressed(_ label: TTTAttributedLabel!, _ link: TTTAttributedLabelLink!) { + // todo: long press options + } + + // MARK: - Navigation + func getMention(for url: URL, text: String) -> Mention? { + return nil + } + + func getHashtag(for url: URL, text: String) -> Hashtag? { + if text.starts(with: "#") { + let tag = String(text.dropFirst()) + return Hashtag(name: tag, url: url) + } else { + return nil + } + } + +} diff --git a/Tusker/Views/HTMLContentLabel.swift b/Tusker/Views/HTMLContentLabel.swift deleted file mode 100644 index 5c6781db..00000000 --- a/Tusker/Views/HTMLContentLabel.swift +++ /dev/null @@ -1,305 +0,0 @@ -// -// StatusContentLabel.swift -// Tusker -// -// Created by Shadowfacts on 8/25/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import SwiftSoup - -protocol HTMLContentLabelDelegate { - - func selected(mention: Mention) - - func selected(tag: Hashtag) - - func selected(url: URL) - -} - -class HTMLContentLabel: UILabel { - - var delegate: HTMLContentLabelDelegate? - - override var text: String? { - didSet { - parseHTML() - } - } - - override var attributedText: NSAttributedString? { - didSet { - updateTextStorage() - } - } - - override var numberOfLines: Int { - didSet { - textContainer.maximumNumberOfLines = numberOfLines - } - } - - override var lineBreakMode: NSLineBreakMode { - didSet { - textContainer.lineBreakMode = lineBreakMode - } - } - -// var status: Status! { -// didSet { -// text = status.content -// } -// } - - private var _customizing = true - - private lazy var textStorage = NSTextStorage() - private lazy var layoutManager = NSLayoutManager() - private lazy var textContainer = NSTextContainer() - - private var selectedLink: (range: NSRange, url: URL)? - - private var links: [NSRange: URL] = [:] - - override init(frame: CGRect) { - super.init(frame: frame) - _customizing = false - setupLabel() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - _customizing = false - setupLabel() - } - - private func setupLabel() { - textStorage.addLayoutManager(layoutManager) - layoutManager.addTextContainer(textContainer) - textContainer.lineFragmentPadding = 0 - textContainer.lineBreakMode = lineBreakMode - textContainer.maximumNumberOfLines = numberOfLines - isUserInteractionEnabled = true - } - - override func awakeFromNib() { - super.awakeFromNib() - updateTextStorage() - } - - override var intrinsicContentSize: CGSize { - let superSize = super.intrinsicContentSize - textContainer.size = CGSize(width: superSize.width, height: .greatestFiniteMagnitude) - let size = layoutManager.usedRect(for: textContainer) - return CGSize(width: ceil(size.width), height: ceil(size.height)) - } - - override func drawText(in rect: CGRect) { - let range = NSRange(location: 0, length: textStorage.length) - - textContainer.size = rect.size - let origin = rect.origin - - layoutManager.drawBackground(forGlyphRange: range, at: origin) - layoutManager.drawGlyphs(forGlyphRange: range, at: origin) - } - - // MARK: - HTML parsing - - private func parseHTML() { - if _customizing { return } - guard let text = text else { return } - - let doc = try! SwiftSoup.parse(text) - let body = doc.body()! - - let (attributedText, links) = attributedTextForHTMLNode(body) - self.links = links - let mutAttrString = NSMutableAttributedString(attributedString: attributedText) - mutAttrString.trimCharactersInSet(.whitespacesAndNewlines) - mutAttrString.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutAttrString.length)) - self.attributedText = mutAttrString - } - - private func attributedTextForHTMLNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) { - switch node { - case let node as TextNode: - return (NSAttributedString(string: node.text()), [:]) - case let node as Element: - var links = [NSRange: URL]() - let attributed = NSMutableAttributedString() - node.getChildNodes().forEach { child in - let (text, childLinks) = attributedTextForHTMLNode(child) - childLinks.forEach { range, url in - let newRange = NSRange(location: range.location + attributed.length, length: range.length) - links[newRange] = url - } - attributed.append(text) - } - - switch node.tagName() { - case "br": - attributed.append(NSAttributedString(string: "\n")) - case "a": - if let link = try? node.attr("href"), - let url = URL(string: link) { - let linkRange = NSRange(location: 0, length: attributed.length) - let linkAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: UIColor.blue - ] - attributed.setAttributes(linkAttributes, range: linkRange) - - links[linkRange] = url - } - case "p": - attributed.append(NSAttributedString(string: "\n\n")) - default: - break - } - - return (attributed, links) - default: - fatalError("Unexpected node type: \(type(of: node))") - } - } - - // MARK: - Text Storage - - private func updateTextStorage() { - if _customizing { return } - guard let attributedText = attributedText, - attributedText.length > 0 else { - links = [:] - textStorage.setAttributedString(NSAttributedString()) - setNeedsDisplay() - return - } - - textStorage.setAttributedString(attributedText) - _customizing = true - text = attributedText.string - _customizing = false - setNeedsDisplay() - } - - // MARK: - Interaction - - func getMention(for url: URL, text: String) -> Mention? { - // todo: figure out how to get account IDs - return nil - } - - func getTag(for url: URL, text: String) -> Hashtag? { - if text.starts(with: "#") { - let tag = String(text.dropFirst()) - return Hashtag(name: tag, url: url) - } else { - return nil - } - } - - private func onTouch(_ touch: UITouch) -> Bool { - let location = touch.location(in: self) - var avoidSuperCall = false - - switch touch.phase { - case .began, .moved: - if let link = link(at: location) { - if link.range.location != selectedLink?.range.location || link.range.length != selectedLink?.range.length { - updateAttributesWhenSelected(false) - selectedLink = link - updateAttributesWhenSelected(true) - } - avoidSuperCall = true - } else { - updateAttributesWhenSelected(false) - selectedLink = nil - } - case .ended: - guard let selectedLink = selectedLink else { return avoidSuperCall } - - let text = String(self.text![Range(selectedLink.range, in: self.text!)!]) - if let delegate = delegate { - if let mention = getMention(for: selectedLink.url, text: text) { - delegate.selected(mention: mention) - } else if let tag = getTag(for: selectedLink.url, text: text) { - delegate.selected(tag: tag) - } else { - delegate.selected(url: selectedLink.url) - } - } - - let when = DispatchTime.now() + Double(Int64(0.25 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) - DispatchQueue.main.asyncAfter(deadline: when) { - self.updateAttributesWhenSelected(false) - self.selectedLink = nil - } - case .cancelled: - updateAttributesWhenSelected(false) - selectedLink = nil - case .stationary: - break - } - - return avoidSuperCall - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesBegan(touches, with: event) - } - - override func touchesMoved(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesMoved(touches, with: event) - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesCancelled(touches, with: event) - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if onTouch(touch) { return } - super.touchesEnded(touches, with: event) - } - - private func link(at location: CGPoint) -> (range: NSRange, url: URL)? { - guard textStorage.length > 0 else { return nil } - - let boundingRect = layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: textStorage.length), in: textContainer) - guard boundingRect.contains(location) else { - return nil - } - - let index = layoutManager.glyphIndex(for: location, in: textContainer) - - for (range, url) in links { - if index >= range.location && index <= range.location + range.length { - return (range, url) - } - } - - return nil - } - - private func updateAttributesWhenSelected(_ isSelected: Bool) { - guard let selectedLink = selectedLink else { return } - - var attributes = textStorage.attributes(at: 0, effectiveRange: nil) - - attributes[.foregroundColor] = isSelected ? nil : UIColor.blue - - textStorage.addAttributes(attributes, range: selectedLink.range) - - setNeedsDisplay() - } - -} diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift index 1ccd2f8c..458bb0ae 100644 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift @@ -40,7 +40,7 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { actionAvatarImageView.layer.masksToBounds = true actionLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(actionPressed))) actionLabel.isUserInteractionEnabled = true - contentLabel.delegate = self + contentLabel.navigationDelegate = self } func updateUIForPreferences() { @@ -129,7 +129,8 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { attachmentsView.isHidden = true } - contentLabel.statusID = status.id +// contentLabel.statusID = status.id + contentLabel.setTextFromHtml(status.content) } func updateTimestamp() { @@ -185,18 +186,16 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { } -extension ActionNotificationTableViewCell: HTMLContentLabelDelegate { - +extension ActionNotificationTableViewCell: ContentLabelNavigationDelegate { func selected(mention: Mention) { delegate?.selected(mention: mention) } - + func selected(tag: Hashtag) { delegate?.selected(tag: tag) } - + func selected(url: URL) { delegate?.selected(url: url) } - } diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index 5356ad7a..386bcb51 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -23,7 +23,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive { @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var followsYouLabel: UILabel! - @IBOutlet weak var noteLabel: HTMLContentLabel! + @IBOutlet weak var noteLabel: StatusContentLabel! var accountID: String! @@ -38,6 +38,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive { avatarImageView.isUserInteractionEnabled = true headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed))) headerImageView.isUserInteractionEnabled = true + noteLabel.navigationDelegate = self } func updateUIForPreferences() { @@ -73,9 +74,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive { } headerImageDownloadTask!.resume() - // todo: HTML parsing - noteLabel.text = account.note - noteLabel.delegate = self + noteLabel.setTextFromHtml(account.note) if let relationship = MastodonCache.relationship(for: accountID) { followsYouLabel.isHidden = !relationship.followedBy @@ -108,8 +107,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive { } -extension ProfileHeaderTableViewCell: HTMLContentLabelDelegate { - +extension ProfileHeaderTableViewCell: ContentLabelNavigationDelegate { func selected(mention: Mention) { delegate?.selected(mention: mention) } @@ -121,5 +119,4 @@ extension ProfileHeaderTableViewCell: HTMLContentLabelDelegate { func selected(url: URL) { delegate?.selected(url: url) } - } diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib index 85b526b9..efd4b747 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib @@ -62,7 +62,7 @@ -