UILabel+YBAttributeTextTapAction.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. //
  2. // UILabel+YBAttributeTextTapAction.m
  3. //
  4. // Created by LYB on 16/7/1.
  5. // Copyright © 2016年 LYB. All rights reserved.
  6. //
  7. #import "UILabel+YBAttributeTextTapAction.h"
  8. #import <objc/runtime.h>
  9. #import <CoreText/CoreText.h>
  10. #import <Foundation/Foundation.h>
  11. @interface YBAttributeModel : NSObject
  12. @property (nonatomic, copy) NSString *str;
  13. @property (nonatomic) NSRange range;
  14. @end
  15. @implementation YBAttributeModel
  16. @end
  17. @implementation UILabel (YBAttributeTextTapAction)
  18. #pragma mark - AssociatedObjects
  19. - (NSMutableArray *)attributeStrings
  20. {
  21. return objc_getAssociatedObject(self, _cmd);
  22. }
  23. - (void)setAttributeStrings:(NSMutableArray *)attributeStrings
  24. {
  25. objc_setAssociatedObject(self, @selector(attributeStrings), attributeStrings, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  26. }
  27. - (NSMutableDictionary *)effectDic
  28. {
  29. return objc_getAssociatedObject(self, _cmd);
  30. }
  31. - (void)setEffectDic:(NSMutableDictionary *)effectDic
  32. {
  33. objc_setAssociatedObject(self, @selector(effectDic), effectDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  34. }
  35. - (BOOL)isTapAction
  36. {
  37. return [objc_getAssociatedObject(self, _cmd) boolValue];
  38. }
  39. - (void)setIsTapAction:(BOOL)isTapAction
  40. {
  41. objc_setAssociatedObject(self, @selector(isTapAction), @(isTapAction), OBJC_ASSOCIATION_ASSIGN);
  42. }
  43. - (void (^)(UILabel *, NSString *, NSRange, NSInteger))tapBlock
  44. {
  45. return objc_getAssociatedObject(self, _cmd);
  46. }
  47. - (void)setTapBlock:(void (^)(UILabel *, NSString *, NSRange, NSInteger))tapBlock
  48. {
  49. objc_setAssociatedObject(self, @selector(tapBlock), tapBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
  50. }
  51. - (id<YBAttributeTapActionDelegate>)delegate
  52. {
  53. return objc_getAssociatedObject(self, _cmd);
  54. }
  55. - (void)setDelegate:(id<YBAttributeTapActionDelegate>)delegate
  56. {
  57. objc_setAssociatedObject(self, @selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN);
  58. }
  59. - (BOOL)enabledTapEffect
  60. {
  61. return [objc_getAssociatedObject(self, _cmd) boolValue];
  62. }
  63. - (void)setEnabledTapEffect:(BOOL)enabledTapEffect
  64. {
  65. objc_setAssociatedObject(self, @selector(enabledTapEffect), @(enabledTapEffect), OBJC_ASSOCIATION_ASSIGN);
  66. self.isTapEffect = enabledTapEffect;
  67. }
  68. - (BOOL)enlargeTapArea
  69. {
  70. NSNumber * number = objc_getAssociatedObject(self, _cmd);
  71. if (!number) {
  72. number = @(YES);
  73. objc_setAssociatedObject(self, _cmd, number, OBJC_ASSOCIATION_ASSIGN);
  74. }
  75. return [number boolValue];
  76. }
  77. - (void)setEnlargeTapArea:(BOOL)enlargeTapArea
  78. {
  79. objc_setAssociatedObject(self, @selector(enlargeTapArea), @(enlargeTapArea), OBJC_ASSOCIATION_ASSIGN);
  80. }
  81. - (UIColor *)tapHighlightedColor
  82. {
  83. UIColor * color = objc_getAssociatedObject(self, _cmd);
  84. if (!color) {
  85. color = [UIColor lightGrayColor];
  86. objc_setAssociatedObject(self, _cmd, color, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  87. }
  88. return color;
  89. }
  90. - (void)setTapHighlightedColor:(UIColor *)tapHighlightedColor
  91. {
  92. objc_setAssociatedObject(self, @selector(tapHighlightedColor), tapHighlightedColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  93. }
  94. - (BOOL)isTapEffect
  95. {
  96. return [objc_getAssociatedObject(self, _cmd) boolValue];
  97. }
  98. - (void)setIsTapEffect:(BOOL)isTapEffect
  99. {
  100. objc_setAssociatedObject(self, @selector(isTapEffect), @(isTapEffect), OBJC_ASSOCIATION_ASSIGN);
  101. }
  102. #pragma mark - mainFunction
  103. - (void)yb_addAttributeTapActionWithStrings:(NSArray <NSString *> *)strings tapClicked:(void (^) (UILabel * label, NSString *string, NSRange range, NSInteger index))tapClick
  104. {
  105. [self yb_removeAttributeTapActions];
  106. [self yb_getRangesWithStrings:strings];
  107. self.userInteractionEnabled = YES;
  108. if (self.tapBlock != tapClick) {
  109. self.tapBlock = tapClick;
  110. }
  111. }
  112. - (void)yb_addAttributeTapActionWithStrings:(NSArray <NSString *> *)strings
  113. delegate:(id <YBAttributeTapActionDelegate> )delegate
  114. {
  115. [self yb_removeAttributeTapActions];
  116. [self yb_getRangesWithStrings:strings];
  117. self.userInteractionEnabled = YES;
  118. if (self.delegate != delegate) {
  119. self.delegate = delegate;
  120. }
  121. }
  122. - (void)yb_addAttributeTapActionWithRanges:(NSArray<NSString *> *)ranges tapClicked:(void (^)(UILabel *, NSString *, NSRange, NSInteger))tapClick
  123. {
  124. [self yb_removeAttributeTapActions];
  125. [self yb_getRangesWithRanges:ranges];
  126. self.userInteractionEnabled = YES;
  127. if (self.tapBlock != tapClick) {
  128. self.tapBlock = tapClick;
  129. }
  130. }
  131. - (void)yb_addAttributeTapActionWithRanges:(NSArray<NSString *> *)ranges delegate:(id<YBAttributeTapActionDelegate>)delegate
  132. {
  133. [self yb_removeAttributeTapActions];
  134. [self yb_getRangesWithRanges:ranges];
  135. self.userInteractionEnabled = YES;
  136. if (self.delegate != delegate) {
  137. self.delegate = delegate;
  138. }
  139. }
  140. - (void)yb_removeAttributeTapActions
  141. {
  142. self.tapBlock = nil;
  143. self.delegate = nil;
  144. self.effectDic = nil;
  145. self.isTapAction = NO;
  146. self.attributeStrings = [NSMutableArray array];
  147. }
  148. #pragma mark - touchAction
  149. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  150. {
  151. if (!self.isTapAction) {
  152. [super touchesBegan:touches withEvent:event];
  153. return;
  154. }
  155. if (objc_getAssociatedObject(self, @selector(enabledTapEffect))) {
  156. self.isTapEffect = self.enabledTapEffect;
  157. }
  158. UITouch *touch = [touches anyObject];
  159. CGPoint point = [touch locationInView:self];
  160. __weak typeof(self) weakSelf = self;
  161. BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) {
  162. if (weakSelf.isTapEffect) {
  163. [weakSelf yb_saveEffectDicWithRange:range];
  164. [weakSelf yb_tapEffectWithStatus:YES];
  165. }
  166. }];
  167. if (!ret) {
  168. [super touchesBegan:touches withEvent:event];
  169. }
  170. }
  171. - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  172. {
  173. if (!self.isTapAction) {
  174. [super touchesEnded:touches withEvent:event];
  175. return;
  176. }
  177. if (self.isTapEffect) {
  178. [self performSelectorOnMainThread:@selector(yb_tapEffectWithStatus:) withObject:nil waitUntilDone:NO];
  179. }
  180. UITouch *touch = [touches anyObject];
  181. CGPoint point = [touch locationInView:self];
  182. __weak typeof(self) weakSelf = self;
  183. BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) {
  184. if (weakSelf.tapBlock) {
  185. weakSelf.tapBlock (weakSelf, string, range, index);
  186. }
  187. if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(yb_tapAttributeInLabel:string:range:index:)]) {
  188. [weakSelf.delegate yb_tapAttributeInLabel:weakSelf string:string range:range index:index];
  189. }
  190. }];
  191. if (!ret) {
  192. [super touchesEnded:touches withEvent:event];
  193. }
  194. }
  195. - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  196. {
  197. if (!self.isTapAction) {
  198. [super touchesCancelled:touches withEvent:event];
  199. return;
  200. }
  201. if (self.isTapEffect) {
  202. [self performSelectorOnMainThread:@selector(yb_tapEffectWithStatus:) withObject:nil waitUntilDone:NO];
  203. }
  204. UITouch *touch = [touches anyObject];
  205. CGPoint point = [touch locationInView:self];
  206. __weak typeof(self) weakSelf = self;
  207. BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) {
  208. if (weakSelf.tapBlock) {
  209. weakSelf.tapBlock (weakSelf, string, range, index);
  210. }
  211. if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(yb_tapAttributeInLabel:string:range:index:)]) {
  212. [weakSelf.delegate yb_tapAttributeInLabel:weakSelf string:string range:range index:index];
  213. }
  214. }];
  215. if (!ret) {
  216. [super touchesCancelled:touches withEvent:event];
  217. }
  218. }
  219. #pragma mark - getTapFrame
  220. - (BOOL)yb_getTapFrameWithTouchPoint:(CGPoint)point result:(void (^) (NSString *string , NSRange range , NSInteger index))resultBlock
  221. {
  222. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedText);
  223. CGMutablePathRef Path = CGPathCreateMutable();
  224. CGPathAddRect(Path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height + 20));
  225. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), Path, NULL);
  226. CFArrayRef lines = CTFrameGetLines(frame);
  227. CGFloat total_height = [self yb_textSizeWithAttributedString:self.attributedText width:self.bounds.size.width numberOfLines:0].height;
  228. if (!lines) {
  229. CFRelease(frame);
  230. CFRelease(framesetter);
  231. CGPathRelease(Path);
  232. return NO;
  233. }
  234. CFIndex count = CFArrayGetCount(lines);
  235. CGPoint origins[count];
  236. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  237. CGAffineTransform transform = [self yb_transformForCoreText];
  238. for (CFIndex i = 0; i < count; i++) {
  239. CGPoint linePoint = origins[i];
  240. CTLineRef line = CFArrayGetValueAtIndex(lines, i);
  241. CGRect flippedRect = [self yb_getLineBounds:line point:linePoint];
  242. CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
  243. CGFloat lineOutSpace = (self.bounds.size.height - total_height) / 2;
  244. rect.origin.y = lineOutSpace + [self yb_getLineOrign:line];
  245. if (self.enlargeTapArea) {
  246. rect.origin.y -= 5;
  247. rect.size.height += 10;
  248. }
  249. if (CGRectContainsPoint(rect, point)) {
  250. CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
  251. CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint);
  252. CGFloat offset;
  253. CTLineGetOffsetForStringIndex(line, index, &offset);
  254. if (offset > relativePoint.x) {
  255. index = index - 1;
  256. }
  257. NSInteger link_count = self.attributeStrings.count;
  258. for (int j = 0; j < link_count; j++) {
  259. YBAttributeModel *model = self.attributeStrings[j];
  260. NSRange link_range = model.range;
  261. if (NSLocationInRange(index, link_range)) {
  262. if (resultBlock) {
  263. resultBlock (model.str , model.range , (NSInteger)j);
  264. }
  265. CFRelease(frame);
  266. CFRelease(framesetter);
  267. CGPathRelease(Path);
  268. return YES;
  269. }
  270. }
  271. }
  272. }
  273. CFRelease(frame);
  274. CFRelease(framesetter);
  275. CGPathRelease(Path);
  276. return NO;
  277. }
  278. - (CGAffineTransform)yb_transformForCoreText
  279. {
  280. return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f);
  281. }
  282. - (CGRect)yb_getLineBounds:(CTLineRef)line point:(CGPoint)point
  283. {
  284. CGFloat ascent = 0.0f;
  285. CGFloat descent = 0.0f;
  286. CGFloat leading = 0.0f;
  287. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  288. CGFloat height = 0.0f;
  289. CFRange range = CTLineGetStringRange(line);
  290. NSAttributedString * attributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(range.location, range.length)];
  291. if ([attributedString.string hasSuffix:@"\n"] && attributedString.string.length > 1) {
  292. attributedString = [attributedString attributedSubstringFromRange:NSMakeRange(0, attributedString.length - 1)];
  293. }
  294. height = [self yb_textSizeWithAttributedString:attributedString width:self.bounds.size.width numberOfLines:0].height;
  295. return CGRectMake(point.x, point.y , width, height);
  296. }
  297. - (CGFloat)yb_getLineOrign:(CTLineRef)line
  298. {
  299. CFRange range = CTLineGetStringRange(line);
  300. if (range.location == 0) {
  301. return 0.;
  302. }else {
  303. NSAttributedString * attributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(0, range.location)];
  304. if ([attributedString.string hasSuffix:@"\n"] && attributedString.string.length > 1) {
  305. attributedString = [attributedString attributedSubstringFromRange:NSMakeRange(0, attributedString.length - 1)];
  306. }
  307. return [self yb_textSizeWithAttributedString:attributedString width:self.bounds.size.width numberOfLines:0].height;
  308. }
  309. }
  310. - (CGSize)yb_textSizeWithAttributedString:(NSAttributedString *)attributedString width:(float)width numberOfLines:(NSInteger)numberOfLines
  311. {
  312. @autoreleasepool {
  313. UILabel *sizeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  314. sizeLabel.numberOfLines = numberOfLines;
  315. sizeLabel.attributedText = attributedString;
  316. CGSize fitSize = [sizeLabel sizeThatFits:CGSizeMake(width, MAXFLOAT)];
  317. return fitSize;
  318. }
  319. }
  320. #pragma mark - tapEffect
  321. - (void)yb_tapEffectWithStatus:(BOOL)status
  322. {
  323. if (self.isTapEffect) {
  324. NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
  325. NSMutableAttributedString *subAtt = [[NSMutableAttributedString alloc] initWithAttributedString:[[self.effectDic allValues] firstObject]];
  326. NSRange range = NSRangeFromString([[self.effectDic allKeys] firstObject]);
  327. if (status) {
  328. [subAtt addAttribute:NSBackgroundColorAttributeName value:self.tapHighlightedColor range:NSMakeRange(0, subAtt.string.length)];
  329. [attStr replaceCharactersInRange:range withAttributedString:subAtt];
  330. }else {
  331. [attStr replaceCharactersInRange:range withAttributedString:subAtt];
  332. }
  333. self.attributedText = attStr;
  334. }
  335. }
  336. - (void)yb_saveEffectDicWithRange:(NSRange)range
  337. {
  338. self.effectDic = [NSMutableDictionary dictionary];
  339. NSAttributedString *subAttribute = [self.attributedText attributedSubstringFromRange:range];
  340. [self.effectDic setObject:subAttribute forKey:NSStringFromRange(range)];
  341. }
  342. #pragma mark - getRange
  343. - (void)yb_getRangesWithStrings:(NSArray <NSString *> *)strings
  344. {
  345. if (self.attributedText == nil) {
  346. self.isTapAction = NO;
  347. return;
  348. }
  349. self.isTapAction = YES;
  350. self.isTapEffect = YES;
  351. __block NSString *totalStr = self.attributedText.string;
  352. self.attributeStrings = [NSMutableArray array];
  353. __weak typeof(self) weakSelf = self;
  354. [strings enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  355. NSRange range = [totalStr rangeOfString:obj];
  356. if (range.length != 0) {
  357. totalStr = [totalStr stringByReplacingCharactersInRange:range withString:[weakSelf yb_getStringWithRange:range]];
  358. YBAttributeModel *model = [YBAttributeModel new];
  359. model.range = range;
  360. model.str = obj;
  361. [weakSelf.attributeStrings addObject:model];
  362. }
  363. }];
  364. }
  365. - (void)yb_getRangesWithRanges:(NSArray <NSString *> *)ranges
  366. {
  367. if (self.attributedText == nil) {
  368. self.isTapAction = NO;
  369. return;
  370. }
  371. self.isTapAction = YES;
  372. self.isTapEffect = YES;
  373. __block NSString *totalStr = self.attributedText.string;
  374. self.attributeStrings = [NSMutableArray array];
  375. __weak typeof(self) weakSelf = self;
  376. [ranges enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  377. NSRange range = NSRangeFromString(obj);
  378. NSAssert(totalStr.length >= range.location + range.length, @"NSRange(%ld,%ld) is out of bounds",range.location,range.length);
  379. NSString * string = [totalStr substringWithRange:range];
  380. YBAttributeModel *model = [YBAttributeModel new];
  381. model.range = range;
  382. model.str = string;
  383. [weakSelf.attributeStrings addObject:model];
  384. }];
  385. }
  386. - (NSString *)yb_getStringWithRange:(NSRange)range
  387. {
  388. NSMutableString *string = [NSMutableString string];
  389. for (int i = 0; i < range.length ; i++) {
  390. [string appendString:@" "];
  391. }
  392. return string;
  393. }
  394. #pragma mark - KVO
  395. - (void)yb_addObserver
  396. {
  397. [self addObserver:self forKeyPath:@"attributedText" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
  398. }
  399. - (void)yb_removeObserver
  400. {
  401. id info = self.observationInfo;
  402. NSString * key = @"attributedText";
  403. NSArray *array = [info valueForKey:@"_observances"];
  404. for (id objc in array) {
  405. id Properties = [objc valueForKeyPath:@"_property"];
  406. NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
  407. if ([key isEqualToString:keyPath]) {
  408. [self removeObserver:self forKeyPath:@"attributedText" context:nil];
  409. }
  410. }
  411. }
  412. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  413. {
  414. if ([keyPath isEqualToString:@"attributedText"]) {
  415. if (self.isTapAction) {
  416. if (![change[NSKeyValueChangeNewKey] isEqual: change[NSKeyValueChangeOldKey]]) {
  417. }
  418. }
  419. }
  420. }
  421. @end