《省钱达人》与《猎豆优选》UI相同版。域名tbk

TTTAttributedLabel.m 81KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888
  1. // TTTAttributedLabel.m
  2. //
  3. // Copyright (c) 2011 Mattt Thompson (http://mattt.me)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. #import "TTTAttributedLabel.h"
  23. #import <QuartzCore/QuartzCore.h>
  24. #import <Availability.h>
  25. #import <objc/runtime.h>
  26. #define kTTTLineBreakWordWrapTextWidthScalingFactor (M_PI / M_E)
  27. static CGFloat const TTTFLOAT_MAX = 100000;
  28. NSString * const kTTTStrikeOutAttributeName = @"TTTStrikeOutAttribute";
  29. NSString * const kTTTBackgroundFillColorAttributeName = @"TTTBackgroundFillColor";
  30. NSString * const kTTTBackgroundFillPaddingAttributeName = @"TTTBackgroundFillPadding";
  31. NSString * const kTTTBackgroundStrokeColorAttributeName = @"TTTBackgroundStrokeColor";
  32. NSString * const kTTTBackgroundLineWidthAttributeName = @"TTTBackgroundLineWidth";
  33. NSString * const kTTTBackgroundCornerRadiusAttributeName = @"TTTBackgroundCornerRadius";
  34. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
  35. const NSTextAlignment TTTTextAlignmentLeft = NSTextAlignmentLeft;
  36. const NSTextAlignment TTTTextAlignmentCenter = NSTextAlignmentCenter;
  37. const NSTextAlignment TTTTextAlignmentRight = NSTextAlignmentRight;
  38. const NSTextAlignment TTTTextAlignmentJustified = NSTextAlignmentJustified;
  39. const NSTextAlignment TTTTextAlignmentNatural = NSTextAlignmentNatural;
  40. const NSLineBreakMode TTTLineBreakByWordWrapping = NSLineBreakByWordWrapping;
  41. const NSLineBreakMode TTTLineBreakByCharWrapping = NSLineBreakByCharWrapping;
  42. const NSLineBreakMode TTTLineBreakByClipping = NSLineBreakByClipping;
  43. const NSLineBreakMode TTTLineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
  44. const NSLineBreakMode TTTLineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
  45. const NSLineBreakMode TTTLineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
  46. typedef NSTextAlignment TTTTextAlignment;
  47. typedef NSLineBreakMode TTTLineBreakMode;
  48. #else
  49. const UITextAlignment TTTTextAlignmentLeft = NSTextAlignmentLeft;
  50. const UITextAlignment TTTTextAlignmentCenter = NSTextAlignmentCenter;
  51. const UITextAlignment TTTTextAlignmentRight = NSTextAlignmentRight;
  52. const UITextAlignment TTTTextAlignmentJustified = NSTextAlignmentJustified;
  53. const UITextAlignment TTTTextAlignmentNatural = NSTextAlignmentNatural;
  54. const UITextAlignment TTTLineBreakByWordWrapping = NSLineBreakByWordWrapping;
  55. const UITextAlignment TTTLineBreakByCharWrapping = NSLineBreakByCharWrapping;
  56. const UITextAlignment TTTLineBreakByClipping = NSLineBreakByClipping;
  57. const UITextAlignment TTTLineBreakByTruncatingHead = NSLineBreakByTruncatingHead;
  58. const UITextAlignment TTTLineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle;
  59. const UITextAlignment TTTLineBreakByTruncatingTail = NSLineBreakByTruncatingTail;
  60. typedef UITextAlignment TTTTextAlignment;
  61. typedef UILineBreakMode TTTLineBreakMode;
  62. #endif
  63. static inline CTTextAlignment CTTextAlignmentFromTTTTextAlignment(TTTTextAlignment alignment) {
  64. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
  65. switch (alignment) {
  66. case NSTextAlignmentLeft: return kCTLeftTextAlignment;
  67. case NSTextAlignmentCenter: return kCTCenterTextAlignment;
  68. case NSTextAlignmentRight: return kCTRightTextAlignment;
  69. default: return kCTNaturalTextAlignment;
  70. }
  71. #else
  72. switch (alignment) {
  73. case UITextAlignmentLeft: return kCTLeftTextAlignment;
  74. case UITextAlignmentCenter: return kCTCenterTextAlignment;
  75. case UITextAlignmentRight: return kCTRightTextAlignment;
  76. default: return kCTNaturalTextAlignment;
  77. }
  78. #endif
  79. }
  80. static inline CTLineBreakMode CTLineBreakModeFromTTTLineBreakMode(TTTLineBreakMode lineBreakMode) {
  81. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
  82. switch (lineBreakMode) {
  83. case NSLineBreakByWordWrapping: return kCTLineBreakByWordWrapping;
  84. case NSLineBreakByCharWrapping: return kCTLineBreakByCharWrapping;
  85. case NSLineBreakByClipping: return kCTLineBreakByClipping;
  86. case NSLineBreakByTruncatingHead: return kCTLineBreakByTruncatingHead;
  87. case NSLineBreakByTruncatingTail: return kCTLineBreakByTruncatingTail;
  88. case NSLineBreakByTruncatingMiddle: return kCTLineBreakByTruncatingMiddle;
  89. default: return 0;
  90. }
  91. #else
  92. return CTLineBreakModeFromUILineBreakMode(lineBreakMode);
  93. #endif
  94. }
  95. #if __IPHONE_OS_VERSION_MAX_ALLOWED < 60000
  96. static inline CTLineBreakMode CTLineBreakModeFromUILineBreakMode(UILineBreakMode lineBreakMode) {
  97. #pragma clang diagnostic push
  98. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  99. switch (lineBreakMode) {
  100. case UILineBreakModeWordWrap: return kCTLineBreakByWordWrapping;
  101. case UILineBreakModeCharacterWrap: return kCTLineBreakByCharWrapping;
  102. case UILineBreakModeClip: return kCTLineBreakByClipping;
  103. case UILineBreakModeHeadTruncation: return kCTLineBreakByTruncatingHead;
  104. case UILineBreakModeTailTruncation: return kCTLineBreakByTruncatingTail;
  105. case UILineBreakModeMiddleTruncation: return kCTLineBreakByTruncatingMiddle;
  106. default: return 0;
  107. }
  108. #pragma clang diagnostic pop
  109. }
  110. #endif
  111. static inline CGFLOAT_TYPE CGFloat_ceil(CGFLOAT_TYPE cgfloat) {
  112. #if CGFLOAT_IS_DOUBLE
  113. return ceil(cgfloat);
  114. #else
  115. return ceilf(cgfloat);
  116. #endif
  117. }
  118. static inline CGFLOAT_TYPE CGFloat_floor(CGFLOAT_TYPE cgfloat) {
  119. #if CGFLOAT_IS_DOUBLE
  120. return floor(cgfloat);
  121. #else
  122. return floorf(cgfloat);
  123. #endif
  124. }
  125. static inline CGFLOAT_TYPE CGFloat_round(CGFLOAT_TYPE cgfloat) {
  126. #if CGFLOAT_IS_DOUBLE
  127. return round(cgfloat);
  128. #else
  129. return roundf(cgfloat);
  130. #endif
  131. }
  132. static inline CGFLOAT_TYPE CGFloat_sqrt(CGFLOAT_TYPE cgfloat) {
  133. #if CGFLOAT_IS_DOUBLE
  134. return sqrt(cgfloat);
  135. #else
  136. return sqrtf(cgfloat);
  137. #endif
  138. }
  139. static inline CGFloat TTTFlushFactorForTextAlignment(NSTextAlignment textAlignment) {
  140. switch (textAlignment) {
  141. case TTTTextAlignmentCenter:
  142. return 0.5f;
  143. case TTTTextAlignmentRight:
  144. return 1.0f;
  145. case TTTTextAlignmentLeft:
  146. default:
  147. return 0.0f;
  148. }
  149. }
  150. static inline NSDictionary * NSAttributedStringAttributesFromLabel(TTTAttributedLabel *label) {
  151. NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary];
  152. if ([NSMutableParagraphStyle class]) {
  153. [mutableAttributes setObject:label.font forKey:(NSString *)kCTFontAttributeName];
  154. [mutableAttributes setObject:label.textColor forKey:(NSString *)kCTForegroundColorAttributeName];
  155. [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName];
  156. NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
  157. paragraphStyle.alignment = label.textAlignment;
  158. paragraphStyle.lineSpacing = label.lineSpacing;
  159. paragraphStyle.minimumLineHeight = label.minimumLineHeight > 0 ? label.minimumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
  160. paragraphStyle.maximumLineHeight = label.maximumLineHeight > 0 ? label.maximumLineHeight : label.font.lineHeight * label.lineHeightMultiple;
  161. paragraphStyle.lineHeightMultiple = label.lineHeightMultiple;
  162. paragraphStyle.firstLineHeadIndent = label.firstLineIndent;
  163. if (label.numberOfLines == 1) {
  164. paragraphStyle.lineBreakMode = label.lineBreakMode;
  165. } else {
  166. paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
  167. }
  168. [mutableAttributes setObject:paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName];
  169. } else {
  170. CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)label.font.fontName, label.font.pointSize, NULL);
  171. [mutableAttributes setObject:(__bridge id)font forKey:(NSString *)kCTFontAttributeName];
  172. CFRelease(font);
  173. [mutableAttributes setObject:(id)[label.textColor CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  174. [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName];
  175. CTTextAlignment alignment = CTTextAlignmentFromTTTTextAlignment(label.textAlignment);
  176. CGFloat lineSpacing = label.lineSpacing;
  177. CGFloat minimumLineHeight = label.minimumLineHeight * label.lineHeightMultiple;
  178. CGFloat maximumLineHeight = label.maximumLineHeight * label.lineHeightMultiple;
  179. CGFloat lineSpacingAdjustment = CGFloat_ceil(label.font.lineHeight - label.font.ascender + label.font.descender);
  180. CGFloat lineHeightMultiple = label.lineHeightMultiple;
  181. CGFloat firstLineIndent = label.firstLineIndent;
  182. CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
  183. if (label.numberOfLines == 1) {
  184. lineBreakMode = CTLineBreakModeFromTTTLineBreakMode(label.lineBreakMode);
  185. }
  186. CTParagraphStyleSetting paragraphStyles[12] = {
  187. {.spec = kCTParagraphStyleSpecifierAlignment, .valueSize = sizeof(CTTextAlignment), .value = (const void *)&alignment},
  188. {.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void *)&lineBreakMode},
  189. {.spec = kCTParagraphStyleSpecifierLineSpacing, .valueSize = sizeof(CGFloat), .value = (const void *)&lineSpacing},
  190. {.spec = kCTParagraphStyleSpecifierMinimumLineSpacing, .valueSize = sizeof(CGFloat), .value = (const void *)&minimumLineHeight},
  191. {.spec = kCTParagraphStyleSpecifierMaximumLineSpacing, .valueSize = sizeof(CGFloat), .value = (const void *)&maximumLineHeight},
  192. {.spec = kCTParagraphStyleSpecifierLineSpacingAdjustment, .valueSize = sizeof (CGFloat), .value = (const void *)&lineSpacingAdjustment},
  193. {.spec = kCTParagraphStyleSpecifierLineHeightMultiple, .valueSize = sizeof(CGFloat), .value = (const void *)&lineHeightMultiple},
  194. {.spec = kCTParagraphStyleSpecifierFirstLineHeadIndent, .valueSize = sizeof(CGFloat), .value = (const void *)&firstLineIndent},
  195. };
  196. CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphStyles, 12);
  197. [mutableAttributes setObject:(__bridge id)paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName];
  198. CFRelease(paragraphStyle);
  199. }
  200. return [NSDictionary dictionaryWithDictionary:mutableAttributes];
  201. }
  202. static inline NSAttributedString * NSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale) {
  203. NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
  204. [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL * __unused stop) {
  205. UIFont *font = (UIFont *)value;
  206. if (font) {
  207. NSString *fontName;
  208. CGFloat pointSize;
  209. if ([font isKindOfClass:[UIFont class]]) {
  210. fontName = font.fontName;
  211. pointSize = font.pointSize;
  212. } else {
  213. fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey));
  214. pointSize = CTFontGetSize((__bridge CTFontRef)font);
  215. }
  216. [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range];
  217. CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)fontName, CGFloat_floor(pointSize * scale), NULL);
  218. [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range];
  219. CFRelease(fontRef);
  220. }
  221. }];
  222. return mutableAttributedString;
  223. }
  224. static inline NSAttributedString * NSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) {
  225. if (!color) {
  226. return attributedString;
  227. }
  228. NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
  229. [mutableAttributedString enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, __unused BOOL *stop) {
  230. BOOL usesColorFromContext = (BOOL)value;
  231. if (usesColorFromContext) {
  232. [mutableAttributedString setAttributes:[NSDictionary dictionaryWithObject:color forKey:(NSString *)kCTForegroundColorAttributeName] range:range];
  233. [mutableAttributedString removeAttribute:(NSString *)kCTForegroundColorFromContextAttributeName range:range];
  234. }
  235. }];
  236. return mutableAttributedString;
  237. }
  238. static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString, CGSize size, NSUInteger numberOfLines) {
  239. CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
  240. CGSize constraints = CGSizeMake(size.width, TTTFLOAT_MAX);
  241. if (numberOfLines == 1) {
  242. // If there is one line, the size that fits is the full width of the line
  243. constraints = CGSizeMake(TTTFLOAT_MAX, TTTFLOAT_MAX);
  244. } else if (numberOfLines > 0) {
  245. // If the line count of the label more than 1, limit the range to size to the number of lines that have been set
  246. CGMutablePathRef path = CGPathCreateMutable();
  247. CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, TTTFLOAT_MAX));
  248. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
  249. CFArrayRef lines = CTFrameGetLines(frame);
  250. if (CFArrayGetCount(lines) > 0) {
  251. NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
  252. CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
  253. CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
  254. rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
  255. }
  256. CFRelease(frame);
  257. CFRelease(path);
  258. }
  259. CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
  260. return CGSizeMake(CGFloat_ceil(suggestedSize.width), CGFloat_ceil(suggestedSize.height));
  261. }
  262. @interface TTTAttributedLabel ()
  263. @property (readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText;
  264. @property (readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText;
  265. @property (readwrite, atomic, strong) NSDataDetector *dataDetector;
  266. @property (readwrite, nonatomic, strong) NSArray *linkModels;
  267. @property (readwrite, nonatomic, strong) TTTAttributedLabelLink *activeLink;
  268. @property (readwrite, nonatomic, strong) NSArray *accessibilityElements;
  269. - (void) longPressGestureDidFire:(UILongPressGestureRecognizer *)sender;
  270. @end
  271. @implementation TTTAttributedLabel {
  272. @private
  273. BOOL _needsFramesetter;
  274. CTFramesetterRef _framesetter;
  275. CTFramesetterRef _highlightFramesetter;
  276. }
  277. @dynamic text;
  278. @synthesize attributedText = _attributedText;
  279. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
  280. #ifndef kCFCoreFoundationVersionNumber_iOS_7_0
  281. #define kCFCoreFoundationVersionNumber_iOS_7_0 847.2
  282. #endif
  283. + (void)load {
  284. static dispatch_once_t onceToken;
  285. dispatch_once(&onceToken, ^{
  286. if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0) {
  287. Class class = [self class];
  288. Class superclass = class_getSuperclass(class);
  289. NSArray *strings = @[
  290. NSStringFromSelector(@selector(isAccessibilityElement)),
  291. NSStringFromSelector(@selector(accessibilityElementCount)),
  292. NSStringFromSelector(@selector(accessibilityElementAtIndex:)),
  293. NSStringFromSelector(@selector(indexOfAccessibilityElement:)),
  294. ];
  295. for (NSString *string in strings) {
  296. SEL selector = NSSelectorFromString(string);
  297. IMP superImplementation = class_getMethodImplementation(superclass, selector);
  298. Method method = class_getInstanceMethod(class, selector);
  299. const char *types = method_getTypeEncoding(method);
  300. class_replaceMethod(class, selector, superImplementation, types);
  301. }
  302. }
  303. });
  304. }
  305. #endif
  306. - (instancetype)initWithFrame:(CGRect)frame {
  307. self = [super initWithFrame:frame];
  308. if (!self) {
  309. return nil;
  310. }
  311. [self commonInit];
  312. return self;
  313. }
  314. - (void)commonInit {
  315. self.userInteractionEnabled = YES;
  316. self.multipleTouchEnabled = NO;
  317. self.textInsets = UIEdgeInsetsZero;
  318. self.lineHeightMultiple = 1.0f;
  319. self.linkModels = [NSArray array];
  320. self.linkBackgroundEdgeInset = UIEdgeInsetsMake(0.0f, -1.0f, 0.0f, -1.0f);
  321. NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary];
  322. [mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName];
  323. NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary];
  324. [mutableActiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
  325. NSMutableDictionary *mutableInactiveLinkAttributes = [NSMutableDictionary dictionary];
  326. [mutableInactiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName];
  327. if ([NSMutableParagraphStyle class]) {
  328. [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  329. [mutableActiveLinkAttributes setObject:[UIColor redColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  330. [mutableInactiveLinkAttributes setObject:[UIColor grayColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  331. } else {
  332. [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  333. [mutableActiveLinkAttributes setObject:(__bridge id)[[UIColor redColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  334. [mutableInactiveLinkAttributes setObject:(__bridge id)[[UIColor grayColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName];
  335. }
  336. self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes];
  337. self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes];
  338. self.inactiveLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableInactiveLinkAttributes];
  339. _extendsLinkTouchArea = YES;
  340. _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
  341. action:@selector(longPressGestureDidFire:)];
  342. self.longPressGestureRecognizer.delegate = self;
  343. [self addGestureRecognizer:self.longPressGestureRecognizer];
  344. }
  345. - (void)dealloc {
  346. if (_framesetter) {
  347. CFRelease(_framesetter);
  348. }
  349. if (_highlightFramesetter) {
  350. CFRelease(_highlightFramesetter);
  351. }
  352. if (_longPressGestureRecognizer) {
  353. [self removeGestureRecognizer:_longPressGestureRecognizer];
  354. }
  355. }
  356. #pragma mark -
  357. + (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString
  358. withConstraints:(CGSize)size
  359. limitedToNumberOfLines:(NSUInteger)numberOfLines
  360. {
  361. if (!attributedString || attributedString.length == 0) {
  362. return CGSizeZero;
  363. }
  364. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString);
  365. CGSize calculatedSize = CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, attributedString, size, numberOfLines);
  366. CFRelease(framesetter);
  367. return calculatedSize;
  368. }
  369. #pragma mark -
  370. - (void)setAttributedText:(NSAttributedString *)text {
  371. if ([text isEqualToAttributedString:_attributedText]) {
  372. return;
  373. }
  374. _attributedText = [text copy];
  375. [self setNeedsFramesetter];
  376. [self setNeedsDisplay];
  377. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
  378. if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) {
  379. [self invalidateIntrinsicContentSize];
  380. }
  381. #endif
  382. [super setText:[self.attributedText string]];
  383. }
  384. - (NSAttributedString *)renderedAttributedText {
  385. if (!_renderedAttributedText) {
  386. self.renderedAttributedText = NSAttributedStringBySettingColorFromContext(self.attributedText, self.textColor);
  387. }
  388. return _renderedAttributedText;
  389. }
  390. - (NSArray *) links {
  391. return [_linkModels valueForKey:@"result"];
  392. }
  393. - (void)setLinkModels:(NSArray *)linkModels {
  394. _linkModels = linkModels;
  395. self.accessibilityElements = nil;
  396. }
  397. - (void)setNeedsFramesetter {
  398. // Reset the rendered attributed text so it has a chance to regenerate
  399. self.renderedAttributedText = nil;
  400. _needsFramesetter = YES;
  401. }
  402. - (CTFramesetterRef)framesetter {
  403. if (_needsFramesetter) {
  404. @synchronized(self) {
  405. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText);
  406. [self setFramesetter:framesetter];
  407. [self setHighlightFramesetter:nil];
  408. _needsFramesetter = NO;
  409. if (framesetter) {
  410. CFRelease(framesetter);
  411. }
  412. }
  413. }
  414. return _framesetter;
  415. }
  416. - (void)setFramesetter:(CTFramesetterRef)framesetter {
  417. if (framesetter) {
  418. CFRetain(framesetter);
  419. }
  420. if (_framesetter) {
  421. CFRelease(_framesetter);
  422. }
  423. _framesetter = framesetter;
  424. }
  425. - (CTFramesetterRef)highlightFramesetter {
  426. return _highlightFramesetter;
  427. }
  428. - (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter {
  429. if (highlightFramesetter) {
  430. CFRetain(highlightFramesetter);
  431. }
  432. if (_highlightFramesetter) {
  433. CFRelease(_highlightFramesetter);
  434. }
  435. _highlightFramesetter = highlightFramesetter;
  436. }
  437. - (CGFloat)leading {
  438. return self.lineSpacing;
  439. }
  440. - (void)setLeading:(CGFloat)leading {
  441. self.lineSpacing = leading;
  442. }
  443. #pragma mark -
  444. - (NSTextCheckingTypes)dataDetectorTypes {
  445. return self.enabledTextCheckingTypes;
  446. }
  447. - (void)setDataDetectorTypes:(NSTextCheckingTypes)dataDetectorTypes {
  448. self.enabledTextCheckingTypes = dataDetectorTypes;
  449. }
  450. - (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)enabledTextCheckingTypes {
  451. if (self.enabledTextCheckingTypes == enabledTextCheckingTypes) {
  452. return;
  453. }
  454. _enabledTextCheckingTypes = enabledTextCheckingTypes;
  455. if (self.enabledTextCheckingTypes) {
  456. self.dataDetector = [NSDataDetector dataDetectorWithTypes:self.enabledTextCheckingTypes error:nil];
  457. } else {
  458. self.dataDetector = nil;
  459. }
  460. }
  461. - (void)addLink:(TTTAttributedLabelLink *)link {
  462. [self addLinks:@[link]];
  463. }
  464. - (void)addLinks:(NSArray *)links {
  465. NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels];
  466. NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
  467. for (TTTAttributedLabelLink *link in links) {
  468. if (link.attributes) {
  469. [mutableAttributedString addAttributes:link.attributes range:link.result.range];
  470. }
  471. }
  472. self.attributedText = mutableAttributedString;
  473. [self setNeedsDisplay];
  474. [mutableLinkModels addObjectsFromArray:links];
  475. self.linkModels = [NSArray arrayWithArray:mutableLinkModels];
  476. }
  477. - (TTTAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result
  478. attributes:(NSDictionary *)attributes
  479. {
  480. return [self addLinksWithTextCheckingResults:@[result] attributes:attributes].firstObject;
  481. }
  482. - (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results
  483. attributes:(NSDictionary *)attributes
  484. {
  485. NSMutableArray *links = [NSMutableArray array];
  486. for (NSTextCheckingResult *result in results) {
  487. NSDictionary *activeAttributes = attributes ? self.activeLinkAttributes : nil;
  488. NSDictionary *inactiveAttributes = attributes ? self.inactiveLinkAttributes : nil;
  489. TTTAttributedLabelLink *link = [[TTTAttributedLabelLink alloc] initWithAttributes:attributes
  490. activeAttributes:activeAttributes
  491. inactiveAttributes:inactiveAttributes
  492. textCheckingResult:result];
  493. [links addObject:link];
  494. }
  495. [self addLinks:links];
  496. return links;
  497. }
  498. - (TTTAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result {
  499. return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes];
  500. }
  501. - (TTTAttributedLabelLink *)addLinkToURL:(NSURL *)url
  502. withRange:(NSRange)range
  503. {
  504. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]];
  505. }
  506. - (TTTAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents
  507. withRange:(NSRange)range
  508. {
  509. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]];
  510. }
  511. - (TTTAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber
  512. withRange:(NSRange)range
  513. {
  514. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]];
  515. }
  516. - (TTTAttributedLabelLink *)addLinkToDate:(NSDate *)date
  517. withRange:(NSRange)range
  518. {
  519. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]];
  520. }
  521. - (TTTAttributedLabelLink *)addLinkToDate:(NSDate *)date
  522. timeZone:(NSTimeZone *)timeZone
  523. duration:(NSTimeInterval)duration
  524. withRange:(NSRange)range
  525. {
  526. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]];
  527. }
  528. - (TTTAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components
  529. withRange:(NSRange)range
  530. {
  531. return [self addLinkWithTextCheckingResult:[NSTextCheckingResult transitInformationCheckingResultWithRange:range components:components]];
  532. }
  533. #pragma mark -
  534. - (BOOL)containslinkAtPoint:(CGPoint)point {
  535. return [self linkAtPoint:point] != nil;
  536. }
  537. - (TTTAttributedLabelLink *)linkAtPoint:(CGPoint)point {
  538. // Stop quickly if none of the points to be tested are in the bounds.
  539. if (!CGRectContainsPoint(CGRectInset(self.bounds, -15.f, -15.f), point) || self.links.count == 0) {
  540. return nil;
  541. }
  542. TTTAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]];
  543. if (!result && self.extendsLinkTouchArea) {
  544. result = [self linkAtRadius:2.5f aroundPoint:point]
  545. ?: [self linkAtRadius:5.f aroundPoint:point]
  546. ?: [self linkAtRadius:7.5f aroundPoint:point]
  547. ?: [self linkAtRadius:12.5f aroundPoint:point]
  548. ?: [self linkAtRadius:15.f aroundPoint:point];
  549. }
  550. return result;
  551. }
  552. - (TTTAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point {
  553. const CGFloat diagonal = CGFloat_sqrt(2 * radius * radius);
  554. const CGPoint deltas[] = {
  555. CGPointMake(0, -radius), CGPointMake(0, radius), // Above and below
  556. CGPointMake(-radius, 0), CGPointMake(radius, 0), // Beside
  557. CGPointMake(-diagonal, -diagonal), CGPointMake(-diagonal, diagonal),
  558. CGPointMake(diagonal, diagonal), CGPointMake(diagonal, -diagonal) // Diagonal
  559. };
  560. const size_t count = sizeof(deltas) / sizeof(CGPoint);
  561. TTTAttributedLabelLink *link = nil;
  562. for (NSInteger i = 0; i < count && link.result == nil; i ++) {
  563. CGPoint currentPoint = CGPointMake(point.x + deltas[i].x, point.y + deltas[i].y);
  564. link = [self linkAtCharacterIndex:[self characterIndexAtPoint:currentPoint]];
  565. }
  566. return link;
  567. }
  568. - (TTTAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx {
  569. // Do not enumerate if the index is outside of the bounds of the text.
  570. if (!NSLocationInRange((NSUInteger)idx, NSMakeRange(0, self.attributedText.length))) {
  571. return nil;
  572. }
  573. NSEnumerator *enumerator = [self.linkModels reverseObjectEnumerator];
  574. TTTAttributedLabelLink *link = nil;
  575. while ((link = [enumerator nextObject])) {
  576. if (NSLocationInRange((NSUInteger)idx, link.result.range)) {
  577. return link;
  578. }
  579. }
  580. return nil;
  581. }
  582. - (CFIndex)characterIndexAtPoint:(CGPoint)p {
  583. if (!CGRectContainsPoint(self.bounds, p)) {
  584. return NSNotFound;
  585. }
  586. CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines];
  587. if (!CGRectContainsPoint(textRect, p)) {
  588. return NSNotFound;
  589. }
  590. // Offset tap coordinates by textRect origin to make them relative to the origin of frame
  591. p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y);
  592. // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
  593. p = CGPointMake(p.x, textRect.size.height - p.y);
  594. CGMutablePathRef path = CGPathCreateMutable();
  595. CGPathAddRect(path, NULL, textRect);
  596. CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL);
  597. if (frame == NULL) {
  598. CFRelease(path);
  599. return NSNotFound;
  600. }
  601. CFArrayRef lines = CTFrameGetLines(frame);
  602. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  603. if (numberOfLines == 0) {
  604. CFRelease(frame);
  605. CFRelease(path);
  606. return NSNotFound;
  607. }
  608. CFIndex idx = NSNotFound;
  609. CGPoint lineOrigins[numberOfLines];
  610. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  611. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
  612. CGPoint lineOrigin = lineOrigins[lineIndex];
  613. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  614. // Get bounding information of line
  615. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  616. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  617. CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
  618. CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
  619. // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
  620. CGFloat flushFactor = TTTFlushFactorForTextAlignment(self.textAlignment);
  621. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width);
  622. lineOrigin.x = penOffset;
  623. // Check if we've already passed the line
  624. if (p.y > yMax) {
  625. break;
  626. }
  627. // Check if the point is within this line vertically
  628. if (p.y >= yMin) {
  629. // Check if the point is within this line horizontally
  630. if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
  631. // Convert CT coordinates to line-relative coordinates
  632. CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
  633. idx = CTLineGetStringIndexForPosition(line, relativePoint);
  634. break;
  635. }
  636. }
  637. }
  638. CFRelease(frame);
  639. CFRelease(path);
  640. return idx;
  641. }
  642. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
  643. - (CGRect)boundingRectForCharacterRange:(NSRange)range {
  644. NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
  645. NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:mutableAttributedString];
  646. NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
  647. [textStorage addLayoutManager:layoutManager];
  648. NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
  649. [layoutManager addTextContainer:textContainer];
  650. NSRange glyphRange;
  651. [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
  652. return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
  653. }
  654. #endif
  655. - (void)drawFramesetter:(CTFramesetterRef)framesetter
  656. attributedString:(NSAttributedString *)attributedString
  657. textRange:(CFRange)textRange
  658. inRect:(CGRect)rect
  659. context:(CGContextRef)c
  660. {
  661. CGMutablePathRef path = CGPathCreateMutable();
  662. CGPathAddRect(path, NULL, rect);
  663. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
  664. [self drawBackground:frame inRect:rect context:c];
  665. CFArrayRef lines = CTFrameGetLines(frame);
  666. NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
  667. BOOL truncateLastLine = (self.lineBreakMode == TTTLineBreakByTruncatingHead || self.lineBreakMode == TTTLineBreakByTruncatingMiddle || self.lineBreakMode == TTTLineBreakByTruncatingTail);
  668. CGPoint lineOrigins[numberOfLines];
  669. CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
  670. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
  671. CGPoint lineOrigin = lineOrigins[lineIndex];
  672. CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
  673. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  674. CGFloat descent = 0.0f;
  675. CTLineGetTypographicBounds((CTLineRef)line, NULL, &descent, NULL);
  676. // Adjust pen offset for flush depending on text alignment
  677. CGFloat flushFactor = TTTFlushFactorForTextAlignment(self.textAlignment);
  678. if (lineIndex == numberOfLines - 1 && truncateLastLine) {
  679. // Check if the range of text in the last line reaches the end of the full attributed string
  680. CFRange lastLineRange = CTLineGetStringRange(line);
  681. if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
  682. // Get correct truncationType and attribute position
  683. CTLineTruncationType truncationType;
  684. CFIndex truncationAttributePosition = lastLineRange.location;
  685. TTTLineBreakMode lineBreakMode = self.lineBreakMode;
  686. // Multiple lines, only use UILineBreakModeTailTruncation
  687. if (numberOfLines != 1) {
  688. lineBreakMode = TTTLineBreakByTruncatingTail;
  689. }
  690. switch (lineBreakMode) {
  691. case TTTLineBreakByTruncatingHead:
  692. truncationType = kCTLineTruncationStart;
  693. break;
  694. case TTTLineBreakByTruncatingMiddle:
  695. truncationType = kCTLineTruncationMiddle;
  696. truncationAttributePosition += (lastLineRange.length / 2);
  697. break;
  698. case TTTLineBreakByTruncatingTail:
  699. default:
  700. truncationType = kCTLineTruncationEnd;
  701. truncationAttributePosition += (lastLineRange.length - 1);
  702. break;
  703. }
  704. #pragma clang diagnostic push
  705. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  706. NSAttributedString *attributedTruncationString = self.attributedTruncationToken;
  707. if (!attributedTruncationString) {
  708. NSString *truncationTokenString = self.truncationTokenString;
  709. if (!truncationTokenString) {
  710. truncationTokenString = @"\u2026"; // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026)
  711. }
  712. NSDictionary *truncationTokenStringAttributes = self.truncationTokenStringAttributes;
  713. if (!truncationTokenStringAttributes) {
  714. truncationTokenStringAttributes = [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL];
  715. }
  716. attributedTruncationString = [[NSAttributedString alloc] initWithString:truncationTokenString attributes:truncationTokenStringAttributes];
  717. }
  718. CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTruncationString);
  719. #pragma clang diagnostic pop
  720. // Append truncationToken to the string
  721. // because if string isn't too long, CT won't add the truncationToken on its own.
  722. // There is no chance of a double truncationToken because CT only adds the
  723. // token if it removes characters (and the one we add will go first)
  724. NSMutableAttributedString *truncationString = [[NSMutableAttributedString alloc] initWithAttributedString:
  725. [attributedString attributedSubstringFromRange:
  726. NSMakeRange((NSUInteger)lastLineRange.location,
  727. (NSUInteger)lastLineRange.length)]];
  728. if (lastLineRange.length > 0) {
  729. // 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.
  730. unichar lastCharacter = [[truncationString string] characterAtIndex:(NSUInteger)(lastLineRange.length - 1)];
  731. if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
  732. [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)];
  733. }
  734. }
  735. [truncationString appendAttributedString:attributedTruncationString];
  736. CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
  737. // Truncate the line in case it is too long.
  738. CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
  739. if (!truncatedLine) {
  740. // If the line is not as wide as the truncationToken, truncatedLine is NULL
  741. truncatedLine = CFRetain(truncationToken);
  742. }
  743. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width);
  744. CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
  745. CTLineDraw(truncatedLine, c);
  746. NSRange linkRange;
  747. if ([attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange]) {
  748. NSRange tokenRange = [truncationString.string rangeOfString:attributedTruncationString.string];
  749. NSRange tokenLinkRange = NSMakeRange((NSUInteger)(lastLineRange.location+lastLineRange.length)-tokenRange.length, (NSUInteger)tokenRange.length);
  750. [self addLinkToURL:[attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange] withRange:tokenLinkRange];
  751. }
  752. CFRelease(truncatedLine);
  753. CFRelease(truncationLine);
  754. CFRelease(truncationToken);
  755. } else {
  756. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
  757. CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
  758. CTLineDraw(line, c);
  759. }
  760. } else {
  761. CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
  762. CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender);
  763. CTLineDraw(line, c);
  764. }
  765. }
  766. [self drawStrike:frame inRect:rect context:c];
  767. CFRelease(frame);
  768. CFRelease(path);
  769. }
  770. - (void)drawBackground:(CTFrameRef)frame
  771. inRect:(CGRect)rect
  772. context:(CGContextRef)c
  773. {
  774. NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
  775. CGPoint origins[[lines count]];
  776. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  777. CFIndex lineIndex = 0;
  778. for (id line in lines) {
  779. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  780. CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading) ;
  781. for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
  782. NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef) glyphRun);
  783. CGColorRef strokeColor = (__bridge CGColorRef)[attributes objectForKey:kTTTBackgroundStrokeColorAttributeName];
  784. CGColorRef fillColor = (__bridge CGColorRef)[attributes objectForKey:kTTTBackgroundFillColorAttributeName];
  785. UIEdgeInsets fillPadding = [[attributes objectForKey:kTTTBackgroundFillPaddingAttributeName] UIEdgeInsetsValue];
  786. CGFloat cornerRadius = [[attributes objectForKey:kTTTBackgroundCornerRadiusAttributeName] floatValue];
  787. CGFloat lineWidth = [[attributes objectForKey:kTTTBackgroundLineWidthAttributeName] floatValue];
  788. if (strokeColor || fillColor) {
  789. CGRect runBounds = CGRectZero;
  790. CGFloat runAscent = 0.0f;
  791. CGFloat runDescent = 0.0f;
  792. runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL) + fillPadding.left + fillPadding.right;
  793. runBounds.size.height = runAscent + runDescent + fillPadding.top + fillPadding.bottom;
  794. CGFloat xOffset = 0.0f;
  795. CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
  796. switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
  797. case kCTRunStatusRightToLeft:
  798. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
  799. break;
  800. default:
  801. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
  802. break;
  803. }
  804. runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset - fillPadding.left - rect.origin.x;
  805. runBounds.origin.y = origins[lineIndex].y + rect.origin.y - fillPadding.bottom - rect.origin.y;
  806. runBounds.origin.y -= runDescent;
  807. // Don't draw higlightedLinkBackground too far to the right
  808. if (CGRectGetWidth(runBounds) > width) {
  809. runBounds.size.width = width;
  810. }
  811. CGPathRef path = [[UIBezierPath bezierPathWithRoundedRect:CGRectInset(UIEdgeInsetsInsetRect(runBounds, self.linkBackgroundEdgeInset), lineWidth, lineWidth) cornerRadius:cornerRadius] CGPath];
  812. CGContextSetLineJoin(c, kCGLineJoinRound);
  813. if (fillColor) {
  814. CGContextSetFillColorWithColor(c, fillColor);
  815. CGContextAddPath(c, path);
  816. CGContextFillPath(c);
  817. }
  818. if (strokeColor) {
  819. CGContextSetStrokeColorWithColor(c, strokeColor);
  820. CGContextAddPath(c, path);
  821. CGContextStrokePath(c);
  822. }
  823. }
  824. }
  825. lineIndex++;
  826. }
  827. }
  828. - (void)drawStrike:(CTFrameRef)frame
  829. inRect:(__unused CGRect)rect
  830. context:(CGContextRef)c
  831. {
  832. NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
  833. CGPoint origins[[lines count]];
  834. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  835. CFIndex lineIndex = 0;
  836. for (id line in lines) {
  837. CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
  838. CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading) ;
  839. for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) {
  840. NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef) glyphRun);
  841. BOOL strikeOut = [[attributes objectForKey:kTTTStrikeOutAttributeName] boolValue];
  842. NSInteger superscriptStyle = [[attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue];
  843. if (strikeOut) {
  844. CGRect runBounds = CGRectZero;
  845. CGFloat runAscent = 0.0f;
  846. CGFloat runDescent = 0.0f;
  847. runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
  848. runBounds.size.height = runAscent + runDescent;
  849. CGFloat xOffset = 0.0f;
  850. CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun);
  851. switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) {
  852. case kCTRunStatusRightToLeft:
  853. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL);
  854. break;
  855. default:
  856. xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL);
  857. break;
  858. }
  859. runBounds.origin.x = origins[lineIndex].x + xOffset;
  860. runBounds.origin.y = origins[lineIndex].y;
  861. runBounds.origin.y -= runDescent;
  862. // Don't draw strikeout too far to the right
  863. if (CGRectGetWidth(runBounds) > width) {
  864. runBounds.size.width = width;
  865. }
  866. switch (superscriptStyle) {
  867. case 1:
  868. runBounds.origin.y -= runAscent * 0.47f;
  869. break;
  870. case -1:
  871. runBounds.origin.y += runAscent * 0.25f;
  872. break;
  873. default:
  874. break;
  875. }
  876. // Use text color, or default to black
  877. id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName];
  878. if (color) {
  879. if ([color isKindOfClass:[UIColor class]]) {
  880. CGContextSetStrokeColorWithColor(c, [color CGColor]);
  881. } else {
  882. CGContextSetStrokeColorWithColor(c, (__bridge CGColorRef)color);
  883. }
  884. } else {
  885. CGContextSetGrayStrokeColor(c, 0.0f, 1.0);
  886. }
  887. CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL);
  888. CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font));
  889. CFRelease(font);
  890. CGFloat y = CGFloat_round(runBounds.origin.y + runBounds.size.height / 2.0f);
  891. CGContextMoveToPoint(c, runBounds.origin.x, y);
  892. CGContextAddLineToPoint(c, runBounds.origin.x + runBounds.size.width, y);
  893. CGContextStrokePath(c);
  894. }
  895. }
  896. lineIndex++;
  897. }
  898. }
  899. #pragma mark - TTTAttributedLabel
  900. - (void)setText:(id)text {
  901. NSParameterAssert(!text || [text isKindOfClass:[NSAttributedString class]] || [text isKindOfClass:[NSString class]]);
  902. if ([text isKindOfClass:[NSString class]]) {
  903. [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil];
  904. return;
  905. }
  906. self.attributedText = text;
  907. self.activeLink = nil;
  908. self.linkModels = [NSArray array];
  909. if (self.attributedText && self.enabledTextCheckingTypes) {
  910. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 50000
  911. __unsafe_unretained __typeof(self)weakSelf = self;
  912. #else
  913. __weak __typeof(self)weakSelf = self;
  914. #endif
  915. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  916. __strong __typeof(weakSelf)strongSelf = weakSelf;
  917. NSDataDetector *dataDetector = strongSelf.dataDetector;
  918. if (dataDetector && [dataDetector respondsToSelector:@selector(matchesInString:options:range:)]) {
  919. NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string] options:0 range:NSMakeRange(0, [(NSAttributedString *)text length])];
  920. if ([results count] > 0) {
  921. dispatch_sync(dispatch_get_main_queue(), ^{
  922. if ([[strongSelf.attributedText string] isEqualToString:[(NSAttributedString *)text string]]) {
  923. [strongSelf addLinksWithTextCheckingResults:results attributes:strongSelf.linkAttributes];
  924. }
  925. });
  926. }
  927. }
  928. });
  929. }
  930. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
  931. if (&NSLinkAttributeName) {
  932. [self.attributedText enumerateAttribute:NSLinkAttributeName inRange:NSMakeRange(0, self.attributedText.length) options:0 usingBlock:^(id value, __unused NSRange range, __unused BOOL *stop) {
  933. if (value) {
  934. NSURL *URL = [value isKindOfClass:[NSString class]] ? [NSURL URLWithString:value] : value;
  935. [self addLinkToURL:URL withRange:range];
  936. }
  937. }];
  938. }
  939. #endif
  940. }
  941. - (void)setText:(id)text
  942. afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString * (^)(NSMutableAttributedString *mutableAttributedString))block
  943. {
  944. NSMutableAttributedString *mutableAttributedString = nil;
  945. if ([text isKindOfClass:[NSString class]]) {
  946. mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:NSAttributedStringAttributesFromLabel(self)];
  947. } else {
  948. mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text];
  949. [mutableAttributedString addAttributes:NSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])];
  950. }
  951. if (block) {
  952. mutableAttributedString = block(mutableAttributedString);
  953. }
  954. [self setText:mutableAttributedString];
  955. }
  956. - (void)setActiveLink:(TTTAttributedLabelLink *)activeLink {
  957. _activeLink = activeLink;
  958. NSDictionary *activeAttributes = activeLink.activeAttributes ?: self.activeLinkAttributes;
  959. if (_activeLink && activeAttributes.count > 0) {
  960. if (!self.inactiveAttributedText) {
  961. self.inactiveAttributedText = [self.attributedText copy];
  962. }
  963. NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy];
  964. if (self.activeLink.result.range.length > 0 && NSLocationInRange(NSMaxRange(self.activeLink.result.range) - 1, NSMakeRange(0, [self.inactiveAttributedText length]))) {
  965. [mutableAttributedString addAttributes:activeAttributes range:self.activeLink.result.range];
  966. }
  967. self.attributedText = mutableAttributedString;
  968. [self setNeedsDisplay];
  969. [CATransaction flush];
  970. } else if (self.inactiveAttributedText) {
  971. self.attributedText = self.inactiveAttributedText;
  972. self.inactiveAttributedText = nil;
  973. [self setNeedsDisplay];
  974. }
  975. }
  976. #pragma mark - UILabel
  977. - (void)setHighlighted:(BOOL)highlighted {
  978. [super setHighlighted:highlighted];
  979. [self setNeedsDisplay];
  980. }
  981. // Fixes crash when loading from a UIStoryboard
  982. - (UIColor *)textColor {
  983. UIColor *color = [super textColor];
  984. if (!color) {
  985. color = [UIColor blackColor];
  986. }
  987. return color;
  988. }
  989. - (void)setTextColor:(UIColor *)textColor {
  990. UIColor *oldTextColor = self.textColor;
  991. [super setTextColor:textColor];
  992. // Redraw to allow any ColorFromContext attributes a chance to update
  993. if (textColor != oldTextColor) {
  994. [self setNeedsFramesetter];
  995. [self setNeedsDisplay];
  996. }
  997. }
  998. - (CGRect)textRectForBounds:(CGRect)bounds
  999. limitedToNumberOfLines:(NSInteger)numberOfLines
  1000. {
  1001. bounds = UIEdgeInsetsInsetRect(bounds, self.textInsets);
  1002. if (!self.attributedText) {
  1003. return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
  1004. }
  1005. CGRect textRect = bounds;
  1006. // 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.
  1007. textRect.size.height = MAX(self.font.lineHeight * MAX(2, numberOfLines), bounds.size.height);
  1008. // Adjust the text to be in the center vertically, if the text size is smaller than bounds
  1009. CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), NULL, textRect.size, NULL);
  1010. textSize = CGSizeMake(CGFloat_ceil(textSize.width), CGFloat_ceil(textSize.height)); // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes
  1011. if (textSize.height < bounds.size.height) {
  1012. CGFloat yOffset = 0.0f;
  1013. switch (self.verticalAlignment) {
  1014. case TTTAttributedLabelVerticalAlignmentCenter:
  1015. yOffset = CGFloat_floor((bounds.size.height - textSize.height) / 2.0f);
  1016. break;
  1017. case TTTAttributedLabelVerticalAlignmentBottom:
  1018. yOffset = bounds.size.height - textSize.height;
  1019. break;
  1020. case TTTAttributedLabelVerticalAlignmentTop:
  1021. default:
  1022. break;
  1023. }
  1024. textRect.origin.y += yOffset;
  1025. }
  1026. return textRect;
  1027. }
  1028. - (void)drawTextInRect:(CGRect)rect {
  1029. CGRect insetRect = UIEdgeInsetsInsetRect(rect, self.textInsets);
  1030. if (!self.attributedText) {
  1031. [super drawTextInRect:insetRect];
  1032. return;
  1033. }
  1034. NSAttributedString *originalAttributedText = nil;
  1035. // Adjust the font size to fit width, if necessarry
  1036. if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) {
  1037. // Use infinite width to find the max width, which will be compared to availableWidth if needed.
  1038. CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(TTTFLOAT_MAX, TTTFLOAT_MAX) : CGSizeZero;
  1039. CGFloat textWidth = [self sizeThatFits:maxSize].width;
  1040. CGFloat availableWidth = self.frame.size.width * self.numberOfLines;
  1041. if (self.numberOfLines > 1 && self.lineBreakMode == TTTLineBreakByWordWrapping) {
  1042. textWidth *= kTTTLineBreakWordWrapTextWidthScalingFactor;
  1043. }
  1044. if (textWidth > availableWidth && textWidth > 0.0f) {
  1045. originalAttributedText = [self.attributedText copy];
  1046. CGFloat scaleFactor = availableWidth / textWidth;
  1047. if ([self respondsToSelector:@selector(minimumScaleFactor)] && self.minimumScaleFactor > scaleFactor) {
  1048. scaleFactor = self.minimumScaleFactor;
  1049. }
  1050. self.attributedText = NSAttributedStringByScalingFontSize(self.attributedText, scaleFactor);
  1051. }
  1052. }
  1053. CGContextRef c = UIGraphicsGetCurrentContext();
  1054. CGContextSaveGState(c);
  1055. {
  1056. CGContextSetTextMatrix(c, CGAffineTransformIdentity);
  1057. // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different)
  1058. CGContextTranslateCTM(c, 0.0f, insetRect.size.height);
  1059. CGContextScaleCTM(c, 1.0f, -1.0f);
  1060. CFRange textRange = CFRangeMake(0, (CFIndex)[self.attributedText length]);
  1061. // First, get the text rect (which takes vertical centering into account)
  1062. CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines];
  1063. // CoreText draws its text aligned to the bottom, so we move the CTM here to take our vertical offsets into account
  1064. CGContextTranslateCTM(c, insetRect.origin.x, insetRect.size.height - textRect.origin.y - textRect.size.height);
  1065. // Second, trace the shadow before the actual text, if we have one
  1066. if (self.shadowColor && !self.highlighted) {
  1067. CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, [self.shadowColor CGColor]);
  1068. } else if (self.highlightedShadowColor) {
  1069. CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, [self.highlightedShadowColor CGColor]);
  1070. }
  1071. // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one)
  1072. if (self.highlightedTextColor && self.highlighted) {
  1073. NSMutableAttributedString *highlightAttributedString = [self.renderedAttributedText mutableCopy];
  1074. [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName value:(id)[self.highlightedTextColor CGColor] range:NSMakeRange(0, highlightAttributedString.length)];
  1075. if (![self highlightFramesetter]) {
  1076. CTFramesetterRef highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString);
  1077. [self setHighlightFramesetter:highlightFramesetter];
  1078. CFRelease(highlightFramesetter);
  1079. }
  1080. [self drawFramesetter:[self highlightFramesetter] attributedString:highlightAttributedString textRange:textRange inRect:textRect context:c];
  1081. } else {
  1082. [self drawFramesetter:[self framesetter] attributedString:self.renderedAttributedText textRange:textRange inRect:textRect context:c];
  1083. }
  1084. // If we adjusted the font size, set it back to its original size
  1085. if (originalAttributedText) {
  1086. // Use ivar directly to avoid clearing out framesetter and renderedAttributedText
  1087. _attributedText = originalAttributedText;
  1088. }
  1089. }
  1090. CGContextRestoreGState(c);
  1091. }
  1092. #pragma mark - UIAccessibilityElement
  1093. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
  1094. - (BOOL)isAccessibilityElement {
  1095. return NO;
  1096. }
  1097. - (NSInteger)accessibilityElementCount {
  1098. return (NSInteger)[[self accessibilityElements] count];
  1099. }
  1100. - (id)accessibilityElementAtIndex:(NSInteger)index {
  1101. return [[self accessibilityElements] objectAtIndex:(NSUInteger)index];
  1102. }
  1103. - (NSInteger)indexOfAccessibilityElement:(id)element {
  1104. return (NSInteger)[[self accessibilityElements] indexOfObject:element];
  1105. }
  1106. - (NSArray *)accessibilityElements {
  1107. if (!_accessibilityElements) {
  1108. @synchronized(self) {
  1109. NSMutableArray *mutableAccessibilityItems = [NSMutableArray array];
  1110. for (TTTAttributedLabelLink *link in self.linkModels) {
  1111. if (link.result.range.location == NSNotFound) {
  1112. continue;
  1113. }
  1114. NSString *sourceText = [self.text isKindOfClass:[NSString class]] ? self.text : [(NSAttributedString *)self.text string];
  1115. NSString *accessibilityLabel = [sourceText substringWithRange:link.result.range];
  1116. NSString *accessibilityValue = link.accessibilityValue;
  1117. if (accessibilityLabel) {
  1118. UIAccessibilityElement *linkElement = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
  1119. linkElement.accessibilityTraits = UIAccessibilityTraitLink;
  1120. linkElement.accessibilityFrame = [self convertRect:[self boundingRectForCharacterRange:link.result.range] toView:self.window];
  1121. linkElement.accessibilityLabel = accessibilityLabel;
  1122. if (![accessibilityLabel isEqualToString:accessibilityValue]) {
  1123. linkElement.accessibilityValue = accessibilityValue;
  1124. }
  1125. [mutableAccessibilityItems addObject:linkElement];
  1126. }
  1127. }
  1128. UIAccessibilityElement *baseElement = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
  1129. baseElement.accessibilityLabel = [super accessibilityLabel];
  1130. baseElement.accessibilityHint = [super accessibilityHint];
  1131. baseElement.accessibilityValue = [super accessibilityValue];
  1132. baseElement.accessibilityFrame = [self convertRect:self.bounds toView:self.window];
  1133. baseElement.accessibilityTraits = [super accessibilityTraits];
  1134. [mutableAccessibilityItems addObject:baseElement];
  1135. self.accessibilityElements = [NSArray arrayWithArray:mutableAccessibilityItems];
  1136. }
  1137. }
  1138. return _accessibilityElements;
  1139. }
  1140. #endif
  1141. #pragma mark - UIView
  1142. - (CGSize)sizeThatFits:(CGSize)size {
  1143. if (!self.attributedText) {
  1144. return [super sizeThatFits:size];
  1145. } else {
  1146. NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
  1147. if (self.attributedTruncationToken) {
  1148. [fullString appendAttributedString:self.attributedTruncationToken];
  1149. }
  1150. NSAttributedString *string = [[NSAttributedString alloc] initWithAttributedString:fullString];
  1151. CGSize labelSize = CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([self framesetter], string, size, (NSUInteger)self.numberOfLines);
  1152. labelSize.width += self.textInsets.left + self.textInsets.right;
  1153. labelSize.height += self.textInsets.top + self.textInsets.bottom;
  1154. return labelSize;
  1155. }
  1156. }
  1157. - (CGSize)intrinsicContentSize {
  1158. // There's an implicit width from the original UILabel implementation
  1159. return [self sizeThatFits:[super intrinsicContentSize]];
  1160. }
  1161. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
  1162. - (void)tintColorDidChange {
  1163. if (!self.inactiveLinkAttributes || [self.inactiveLinkAttributes count] == 0) {
  1164. return;
  1165. }
  1166. BOOL isInactive = (self.tintAdjustmentMode == UIViewTintAdjustmentModeDimmed);
  1167. NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy];
  1168. for (TTTAttributedLabelLink *link in self.linkModels) {
  1169. NSDictionary *attributesToRemove = isInactive ? link.attributes : link.inactiveAttributes;
  1170. NSDictionary *attributesToAdd = isInactive ? link.inactiveAttributes : link.attributes;
  1171. [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop) {
  1172. if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
  1173. [mutableAttributedString removeAttribute:name range:link.result.range];
  1174. }
  1175. }];
  1176. if (attributesToAdd) {
  1177. if (NSMaxRange(link.result.range) <= mutableAttributedString.length) {
  1178. [mutableAttributedString addAttributes:attributesToAdd range:link.result.range];
  1179. }
  1180. }
  1181. }
  1182. self.attributedText = mutableAttributedString;
  1183. [self setNeedsDisplay];
  1184. }
  1185. #endif
  1186. - (UIView *)hitTest:(CGPoint)point
  1187. withEvent:(UIEvent *)event
  1188. {
  1189. if (![self linkAtPoint:point] || !self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
  1190. return [super hitTest:point withEvent:event];
  1191. }
  1192. return self;
  1193. }
  1194. #pragma mark - UIResponder
  1195. - (BOOL)canBecomeFirstResponder {
  1196. return YES;
  1197. }
  1198. - (BOOL)canPerformAction:(SEL)action
  1199. withSender:(__unused id)sender
  1200. {
  1201. return (action == @selector(copy:));
  1202. }
  1203. - (void)touchesBegan:(NSSet *)touches
  1204. withEvent:(UIEvent *)event
  1205. {
  1206. UITouch *touch = [touches anyObject];
  1207. self.activeLink = [self linkAtPoint:[touch locationInView:self]];
  1208. if (!self.activeLink) {
  1209. [super touchesBegan:touches withEvent:event];
  1210. }
  1211. }
  1212. - (void)touchesMoved:(NSSet *)touches
  1213. withEvent:(UIEvent *)event
  1214. {
  1215. if (self.activeLink) {
  1216. UITouch *touch = [touches anyObject];
  1217. if (self.activeLink != [self linkAtPoint:[touch locationInView:self]]) {
  1218. self.activeLink = nil;
  1219. }
  1220. } else {
  1221. [super touchesMoved:touches withEvent:event];
  1222. }
  1223. }
  1224. - (void)touchesEnded:(NSSet *)touches
  1225. withEvent:(UIEvent *)event
  1226. {
  1227. if (self.activeLink) {
  1228. if (self.activeLink.linkTapBlock) {
  1229. self.activeLink.linkTapBlock(self, self.activeLink);
  1230. self.activeLink = nil;
  1231. return;
  1232. }
  1233. NSTextCheckingResult *result = self.activeLink.result;
  1234. self.activeLink = nil;
  1235. switch (result.resultType) {
  1236. case NSTextCheckingTypeLink:
  1237. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) {
  1238. [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL];
  1239. return;
  1240. }
  1241. break;
  1242. case NSTextCheckingTypeAddress:
  1243. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) {
  1244. [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents];
  1245. return;
  1246. }
  1247. break;
  1248. case NSTextCheckingTypePhoneNumber:
  1249. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) {
  1250. [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber];
  1251. return;
  1252. }
  1253. break;
  1254. case NSTextCheckingTypeDate:
  1255. if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:timeZone:duration:)]) {
  1256. [self.delegate attributedLabel:self didSelectLinkWithDate:result.date timeZone:result.timeZone duration:result.duration];
  1257. return;
  1258. } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) {
  1259. [self.delegate attributedLabel:self didSelectLinkWithDate:result.date];
  1260. return;
  1261. }
  1262. break;
  1263. case NSTextCheckingTypeTransitInformation:
  1264. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTransitInformation:)]) {
  1265. [self.delegate attributedLabel:self didSelectLinkWithTransitInformation:result.components];
  1266. return;
  1267. }
  1268. default:
  1269. break;
  1270. }
  1271. // Fallback to `attributedLabel:didSelectLinkWithTextCheckingResult:` if no other delegate method matched.
  1272. if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTextCheckingResult:)]) {
  1273. [self.delegate attributedLabel:self didSelectLinkWithTextCheckingResult:result];
  1274. }
  1275. } else {
  1276. [super touchesEnded:touches withEvent:event];
  1277. }
  1278. }
  1279. - (void)touchesCancelled:(NSSet *)touches
  1280. withEvent:(UIEvent *)event
  1281. {
  1282. if (self.activeLink) {
  1283. self.activeLink = nil;
  1284. } else {
  1285. [super touchesCancelled:touches withEvent:event];
  1286. }
  1287. }
  1288. #pragma mark - UIGestureRecognizerDelegate
  1289. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
  1290. return [self containslinkAtPoint:[touch locationInView:self]];
  1291. }
  1292. #pragma mark - UILongPressGestureRecognizer
  1293. - (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender {
  1294. switch (sender.state) {
  1295. case UIGestureRecognizerStateBegan: {
  1296. CGPoint touchPoint = [sender locationInView:self];
  1297. TTTAttributedLabelLink *link = [self linkAtPoint:touchPoint];
  1298. if (link) {
  1299. if (link.linkLongPressBlock) {
  1300. link.linkLongPressBlock(self, link);
  1301. return;
  1302. }
  1303. NSTextCheckingResult *result = link.result;
  1304. if (!result) {
  1305. return;
  1306. }
  1307. switch (result.resultType) {
  1308. case NSTextCheckingTypeLink:
  1309. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithURL:atPoint:)]) {
  1310. [self.delegate attributedLabel:self didLongPressLinkWithURL:result.URL atPoint:touchPoint];
  1311. return;
  1312. }
  1313. break;
  1314. case NSTextCheckingTypeAddress:
  1315. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithAddress:atPoint:)]) {
  1316. [self.delegate attributedLabel:self didLongPressLinkWithAddress:result.addressComponents atPoint:touchPoint];
  1317. return;
  1318. }
  1319. break;
  1320. case NSTextCheckingTypePhoneNumber:
  1321. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithPhoneNumber:atPoint:)]) {
  1322. [self.delegate attributedLabel:self didLongPressLinkWithPhoneNumber:result.phoneNumber atPoint:touchPoint];
  1323. return;
  1324. }
  1325. break;
  1326. case NSTextCheckingTypeDate:
  1327. if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:timeZone:duration:atPoint:)]) {
  1328. [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date timeZone:result.timeZone duration:result.duration atPoint:touchPoint];
  1329. return;
  1330. } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:atPoint:)]) {
  1331. [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date atPoint:touchPoint];
  1332. return;
  1333. }
  1334. break;
  1335. case NSTextCheckingTypeTransitInformation:
  1336. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTransitInformation:atPoint:)]) {
  1337. [self.delegate attributedLabel:self didLongPressLinkWithTransitInformation:result.components atPoint:touchPoint];
  1338. return;
  1339. }
  1340. default:
  1341. break;
  1342. }
  1343. // Fallback to `attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:` if no other delegate method matched.
  1344. if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:)]) {
  1345. [self.delegate attributedLabel:self didLongPressLinkWithTextCheckingResult:result atPoint:touchPoint];
  1346. }
  1347. }
  1348. break;
  1349. }
  1350. default:
  1351. break;
  1352. }
  1353. }
  1354. #pragma mark - UIResponderStandardEditActions
  1355. - (void)copy:(__unused id)sender {
  1356. [[UIPasteboard generalPasteboard] setString:self.text];
  1357. }
  1358. #pragma mark - NSCoding
  1359. - (void)encodeWithCoder:(NSCoder *)coder {
  1360. [super encodeWithCoder:coder];
  1361. [coder encodeObject:@(self.enabledTextCheckingTypes) forKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))];
  1362. [coder encodeObject:self.linkModels forKey:NSStringFromSelector(@selector(linkModels))];
  1363. if ([NSMutableParagraphStyle class]) {
  1364. [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))];
  1365. [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1366. [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1367. }
  1368. [coder encodeObject:@(self.shadowRadius) forKey:NSStringFromSelector(@selector(shadowRadius))];
  1369. [coder encodeObject:@(self.highlightedShadowRadius) forKey:NSStringFromSelector(@selector(highlightedShadowRadius))];
  1370. [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1371. [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1372. [coder encodeObject:@(self.kern) forKey:NSStringFromSelector(@selector(kern))];
  1373. [coder encodeObject:@(self.firstLineIndent) forKey:NSStringFromSelector(@selector(firstLineIndent))];
  1374. [coder encodeObject:@(self.lineSpacing) forKey:NSStringFromSelector(@selector(lineSpacing))];
  1375. [coder encodeObject:@(self.lineHeightMultiple) forKey:NSStringFromSelector(@selector(lineHeightMultiple))];
  1376. [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))];
  1377. [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))];
  1378. #pragma clang diagnostic push
  1379. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  1380. [coder encodeObject:self.truncationTokenString forKey:NSStringFromSelector(@selector(truncationTokenString))];
  1381. #pragma clang diagnostic pop
  1382. [coder encodeObject:NSStringFromUIEdgeInsets(self.linkBackgroundEdgeInset) forKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))];
  1383. [coder encodeObject:self.attributedText forKey:NSStringFromSelector(@selector(attributedText))];
  1384. [coder encodeObject:self.text forKey:NSStringFromSelector(@selector(text))];
  1385. }
  1386. - (id)initWithCoder:(NSCoder *)coder {
  1387. self = [super initWithCoder:coder];
  1388. if (!self) {
  1389. return nil;
  1390. }
  1391. [self commonInit];
  1392. if ([coder containsValueForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]) {
  1393. self.enabledTextCheckingTypes = [[coder decodeObjectForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))] unsignedLongLongValue];
  1394. }
  1395. if ([NSMutableParagraphStyle class]) {
  1396. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))]) {
  1397. self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))];
  1398. }
  1399. if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))]) {
  1400. self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))];
  1401. }
  1402. if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]) {
  1403. self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))];
  1404. }
  1405. }
  1406. if ([coder containsValueForKey:NSStringFromSelector(@selector(links))]) {
  1407. NSArray *oldLinks = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))];
  1408. [self addLinksWithTextCheckingResults:oldLinks attributes:nil];
  1409. }
  1410. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkModels))]) {
  1411. self.linkModels = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkModels))];
  1412. }
  1413. if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))]) {
  1414. self.shadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(shadowRadius))] floatValue];
  1415. }
  1416. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))]) {
  1417. self.highlightedShadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowRadius))] floatValue];
  1418. }
  1419. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]) {
  1420. self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))];
  1421. }
  1422. if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))]) {
  1423. self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))];
  1424. }
  1425. if ([coder containsValueForKey:NSStringFromSelector(@selector(kern))]) {
  1426. self.kern = [[coder decodeObjectForKey:NSStringFromSelector(@selector(kern))] floatValue];
  1427. }
  1428. if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))]) {
  1429. self.firstLineIndent = [[coder decodeObjectForKey:NSStringFromSelector(@selector(firstLineIndent))] floatValue];
  1430. }
  1431. if ([coder containsValueForKey:NSStringFromSelector(@selector(lineSpacing))]) {
  1432. self.lineSpacing = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineSpacing))] floatValue];
  1433. }
  1434. if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumLineHeight))]) {
  1435. self.minimumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(minimumLineHeight))] floatValue];
  1436. }
  1437. if ([coder containsValueForKey:NSStringFromSelector(@selector(maximumLineHeight))]) {
  1438. self.maximumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(maximumLineHeight))] floatValue];
  1439. }
  1440. if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))]) {
  1441. self.lineHeightMultiple = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineHeightMultiple))] floatValue];
  1442. }
  1443. if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))]) {
  1444. self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))];
  1445. }
  1446. if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))]) {
  1447. self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))];
  1448. }
  1449. #pragma clang diagnostic push
  1450. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  1451. if ([coder containsValueForKey:NSStringFromSelector(@selector(truncationTokenString))]) {
  1452. self.truncationTokenString = [coder decodeObjectForKey:NSStringFromSelector(@selector(truncationTokenString))];
  1453. }
  1454. #pragma clang diagnostic pop
  1455. if ([coder containsValueForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]) {
  1456. self.linkBackgroundEdgeInset = UIEdgeInsetsFromString([coder decodeObjectForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]);
  1457. }
  1458. if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedText))]) {
  1459. self.attributedText = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedText))];
  1460. } else {
  1461. self.text = super.text;
  1462. }
  1463. return self;
  1464. }
  1465. @end
  1466. #pragma mark - TTTAttributedLabelLink
  1467. @implementation TTTAttributedLabelLink
  1468. - (instancetype)initWithAttributes:(NSDictionary *)attributes
  1469. activeAttributes:(NSDictionary *)activeAttributes
  1470. inactiveAttributes:(NSDictionary *)inactiveAttributes
  1471. textCheckingResult:(NSTextCheckingResult *)result {
  1472. if ((self = [super init])) {
  1473. _result = result;
  1474. _attributes = [attributes copy];
  1475. _activeAttributes = [activeAttributes copy];
  1476. _inactiveAttributes = [inactiveAttributes copy];
  1477. }
  1478. return self;
  1479. }
  1480. - (instancetype)initWithAttributesFromLabel:(TTTAttributedLabel*)label
  1481. textCheckingResult:(NSTextCheckingResult *)result {
  1482. return [self initWithAttributes:label.linkAttributes
  1483. activeAttributes:label.activeLinkAttributes
  1484. inactiveAttributes:label.inactiveLinkAttributes
  1485. textCheckingResult:result];
  1486. }
  1487. #pragma mark - Accessibility
  1488. - (NSString *) accessibilityValue {
  1489. if ([_accessibilityValue length] == 0) {
  1490. switch (self.result.resultType) {
  1491. case NSTextCheckingTypeLink:
  1492. _accessibilityValue = self.result.URL.absoluteString;
  1493. break;
  1494. case NSTextCheckingTypePhoneNumber:
  1495. _accessibilityValue = self.result.phoneNumber;
  1496. break;
  1497. case NSTextCheckingTypeDate:
  1498. _accessibilityValue = [NSDateFormatter localizedStringFromDate:self.result.date
  1499. dateStyle:NSDateFormatterLongStyle
  1500. timeStyle:NSDateFormatterLongStyle];
  1501. break;
  1502. default:
  1503. break;
  1504. }
  1505. }
  1506. return _accessibilityValue;
  1507. }
  1508. #pragma mark - NSCoding
  1509. - (void)encodeWithCoder:(NSCoder *)aCoder {
  1510. [aCoder encodeObject:self.result forKey:NSStringFromSelector(@selector(result))];
  1511. [aCoder encodeObject:self.attributes forKey:NSStringFromSelector(@selector(attributes))];
  1512. [aCoder encodeObject:self.activeAttributes forKey:NSStringFromSelector(@selector(activeAttributes))];
  1513. [aCoder encodeObject:self.inactiveAttributes forKey:NSStringFromSelector(@selector(inactiveAttributes))];
  1514. [aCoder encodeObject:self.accessibilityValue forKey:NSStringFromSelector(@selector(accessibilityValue))];
  1515. }
  1516. - (id)initWithCoder:(NSCoder *)aDecoder {
  1517. if ((self = [super init])) {
  1518. _result = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(result))];
  1519. _attributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(attributes))];
  1520. _activeAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(activeAttributes))];
  1521. _inactiveAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(inactiveAttributes))];
  1522. self.accessibilityValue = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(accessibilityValue))];
  1523. }
  1524. return self;
  1525. }
  1526. @end