| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797 |
- // 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];
- }
|