Files
featherVoice/TUIKit/TIMCommon/CommonModel/TUIAttributedLabel.m
2025-08-08 10:49:36 +08:00

1798 lines
78 KiB
Objective-C

// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
// TUIAttributedLabel.m
#import "TUIAttributedLabel.h"
#import <Availability.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
#define kTUILineBreakWordWrapTextWidthScalingFactor (M_PI / M_E)
static CGFloat const TUIFLOAT_MAX = 100000;
NSString *const kTUIStrikeOutAttributeName = @"TUIStrikeOutAttribute";
NSString *const kTUIBackgroundFillColorAttributeName = @"TUIBackgroundFillColor";
NSString *const kTUIBackgroundFillPaddingAttributeName = @"TUIBackgroundFillPadding";
NSString *const kTUIBackgroundStrokeColorAttributeName = @"TUIBackgroundStrokeColor";
NSString *const kTUIBackgroundLineWidthAttributeName = @"TUIBackgroundLineWidth";
NSString *const kTUIBackgroundCornerRadiusAttributeName = @"TUIBackgroundCornerRadius";
const NSTextAlignment TUITextAlignmentLeft = NSTextAlignmentLeft;
const NSTextAlignment TUITextAlignmentCenter = NSTextAlignmentCenter;
const NSTextAlignment TUITextAlignmentRight = NSTextAlignmentRight;
const NSTextAlignment TUITextAlignmentJustified = NSTextAlignmentJustified;
const NSTextAlignment TUITextAlignmentNatural = NSTextAlignmentNatural;
const NSLineBreakMode TUILineBreakByWordWrapping = NSLineBreakByWordWrapping;
const NSLineBreakMode TUILineBreakByCharWrapping = NSLineBreakByCharWrapping;
const NSLineBreakMode TUILineBreakByClipping = NSLineBreakByClipping;
const NSLineBreakMode TUILineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
const NSLineBreakMode TUILineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
const NSLineBreakMode TUILineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
typedef NSTextAlignment TUITextAlignment;
typedef NSLineBreakMode TUILineBreakMode;
static inline CGFLOAT_TYPE formatCGFloatCeil(CGFLOAT_TYPE cgfloat) {
#if CGFLOAT_IS_DOUBLE
return ceil(cgfloat);
#else
return ceilf(cgfloat);
#endif
}
static inline CGFLOAT_TYPE formatCGFloatFloor(CGFLOAT_TYPE cgfloat) {
#if CGFLOAT_IS_DOUBLE
return floor(cgfloat);
#else
return floorf(cgfloat);
#endif
}
static inline CGFLOAT_TYPE formatCGFloatRound(CGFLOAT_TYPE cgfloat) {
#if CGFLOAT_IS_DOUBLE
return round(cgfloat);
#else
return roundf(cgfloat);
#endif
}
static inline CGFLOAT_TYPE formatCGFloatSqrt(CGFLOAT_TYPE cgfloat) {
#if CGFLOAT_IS_DOUBLE
return sqrt(cgfloat);
#else
return sqrtf(cgfloat);
#endif
}
static inline CGFloat flushFactorForTextAlignment(NSTextAlignment textAlignment) {
switch (textAlignment) {
case TUITextAlignmentCenter:
return 0.5f;
case TUITextAlignmentRight:
return 1.0f;
case TUITextAlignmentLeft:
default:
return 0.0f;
}
}
static inline NSDictionary *formatNSAttributedStringAttributesFromLabel(TUIAttributedLabel *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 formatCGColorRefFromColor(id color);
static inline NSDictionary *convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes);
static inline NSAttributedString *formatNSAttributedStringByScalingFontSize(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, formatCGFloatFloor(pointSize * scale), NULL);
[mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range];
CFRelease(fontRef);
}
}];
return mutableAttributedString;
}
static inline NSAttributedString *formatNSAttributedStringBySettingColorFromContext(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 formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString,
CGSize size, NSUInteger numberOfLines) {
CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
CGSize constraints = CGSizeMake(size.width, TUIFLOAT_MAX);
if (numberOfLines == 1) {
// If there is one line, the size that fits is the full width of the line
constraints = CGSizeMake(TUIFLOAT_MAX, TUIFLOAT_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, TUIFLOAT_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(formatCGFloatCeil(suggestedSize.width), formatCGFloatCeil(suggestedSize.height));
}
@interface TUIAccessibilityElement : UIAccessibilityElement
@property(nonatomic, weak) UIView *superview;
@property(nonatomic, assign) CGRect boundingRect;
@end
@implementation TUIAccessibilityElement
- (CGRect)accessibilityFrame {
return UIAccessibilityConvertFrameToScreenCoordinates(self.boundingRect, self.superview);
}
@end
@interface TUIAttributedLabel ()
@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) TUIAttributedLabelLink *activeLink;
@property(readwrite, nonatomic, strong) NSArray *accessibilityElements;
- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender;
@end
@implementation TUIAttributedLabel {
@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 = formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(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 = formatNSAttributedStringBySettingColorFromContext(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:(TUIAttributedLabelLink *)link {
[self addLinks:@[ link ]];
}
- (void)addLinks:(NSArray *)links {
NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels];
NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
for (TUIAttributedLabelLink *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];
}
- (TUIAttributedLabelLink *)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;
TUIAttributedLabelLink *link = [[TUIAttributedLabelLink alloc] initWithAttributes:attributes
activeAttributes:activeAttributes
inactiveAttributes:inactiveAttributes
textCheckingResult:result];
[links addObject:link];
}
[self addLinks:links];
return links;
}
- (TUIAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result {
return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes];
}
- (TUIAttributedLabelLink *)addLinkToURL:(NSURL *)url withRange:(NSRange)range {
return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]];
}
- (TUIAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents withRange:(NSRange)range {
return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]];
}
- (TUIAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber withRange:(NSRange)range {
return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]];
}
- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date withRange:(NSRange)range {
return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]];
}
- (TUIAttributedLabelLink *)addLinkToDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone duration:(NSTimeInterval)duration withRange:(NSRange)range {
return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]];
}
- (TUIAttributedLabelLink *)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;
}
- (TUIAttributedLabelLink *)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;
}
TUIAttributedLabelLink *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;
}
- (TUIAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point {
const CGFloat diagonal = formatCGFloatSqrt(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);
TUIAttributedLabelLink *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;
}
- (TUIAttributedLabelLink *)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];
TUIAttributedLabelLink *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 = flushFactorForTextAlignment(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 == TUILineBreakByTruncatingHead || self.lineBreakMode == TUILineBreakByTruncatingMiddle ||
self.lineBreakMode == TUILineBreakByTruncatingTail);
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 = flushFactorForTextAlignment(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;
TUILineBreakMode lineBreakMode = self.lineBreakMode;
// Multiple lines, only use UILineBreakModeTailTruncation
if (numberOfLines != 1) {
lineBreakMode = TUILineBreakByTruncatingTail;
}
switch (lineBreakMode) {
case TUILineBreakByTruncatingHead:
truncationType = kCTLineTruncationStart;
break;
case TUILineBreakByTruncatingMiddle:
truncationType = kCTLineTruncationMiddle;
truncationAttributePosition += (lastLineRange.length / 2);
break;
case TUILineBreakByTruncatingTail:
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 = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundStrokeColorAttributeName]);
CGColorRef fillColor = formatCGColorRefFromColor([attributes objectForKey:kTUIBackgroundFillColorAttributeName]);
UIEdgeInsets fillPadding = [[attributes objectForKey:kTUIBackgroundFillPaddingAttributeName] UIEdgeInsetsValue];
CGFloat cornerRadius = [[attributes objectForKey:kTUIBackgroundCornerRadiusAttributeName] floatValue];
CGFloat lineWidth = [[attributes objectForKey:kTUIBackgroundLineWidthAttributeName] 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:kTUIStrikeOutAttributeName] 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, formatCGColorRefFromColor(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 = formatCGFloatRound(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 - TUIAttributedLabel
- (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:formatNSAttributedStringAttributesFromLabel(self)];
} else {
mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text];
[mutableAttributedString addAttributes:formatNSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])];
}
if (block) {
mutableAttributedString = block(mutableAttributedString);
}
[self setText:mutableAttributedString];
}
- (void)setActiveLink:(TUIAttributedLabelLink *)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(formatCGFloatCeil(textSize.width),
formatCGFloatCeil(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 TUIAttributedLabelVerticalAlignmentCenter:
yOffset = formatCGFloatFloor((bounds.size.height - textSize.height) / 2.0f);
break;
case TUIAttributedLabelVerticalAlignmentBottom:
yOffset = bounds.size.height - textSize.height;
break;
case TUIAttributedLabelVerticalAlignmentTop:
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(TUIFLOAT_MAX, TUIFLOAT_MAX) : CGSizeZero;
CGFloat textWidth = [self sizeThatFits:maxSize].width;
CGFloat availableWidth = self.frame.size.width * self.numberOfLines;
if (self.numberOfLines > 1 && self.lineBreakMode == TUILineBreakByWordWrapping) {
textWidth *= kTUILineBreakWordWrapTextWidthScalingFactor;
}
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 = formatNSAttributedStringByScalingFontSize(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 (TUIAttributedLabelLink *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) {
TUIAccessibilityElement *linkElement = [[TUIAccessibilityElement 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];
}
}
TUIAccessibilityElement *baseElement = [[TUIAccessibilityElement 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 =
formatCTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([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 (TUIAttributedLabelLink *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];
TUIAttributedLabelLink *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 - TUIAttributedLabelLink
@implementation TUIAttributedLabelLink
- (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:(TUIAttributedLabel *)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 formatCGColorRefFromColor(id color) { return [color isKindOfClass:[UIColor class]] ? [color CGColor] : (__bridge CGColorRef)color; }
static inline CTFontRef formatCTFontRefFromUIFont(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 *convertMap = @{
NSFontAttributeName : (NSString *)kCTFontAttributeName,
NSBackgroundColorAttributeName : (NSString *)kTUIBackgroundFillColorAttributeName,
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 = [convertMap objectForKey:key] ?: key;
if (![NSMutableParagraphStyle class]) {
if ([value isKindOfClass:[UIFont class]]) {
value = (__bridge id)formatCTFontRefFromUIFont(value);
} else if ([value isKindOfClass:[UIColor class]]) {
value = (__bridge id)((UIColor *)value).CGColor;
}
}
[mutableAttributes setObject:value forKey:key];
}];
return [NSDictionary dictionaryWithDictionary:mutableAttributes];
}