UILabel+YBAttributeTextTapAction.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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_getRangesWithStrings:strings];
  106. self.userInteractionEnabled = YES;
  107. if (self.tapBlock != tapClick) {
  108. self.tapBlock = tapClick;
  109. }
  110. }
  111. - (void)yb_addAttributeTapActionWithStrings:(NSArray <NSString *> *)strings
  112. delegate:(id <YBAttributeTapActionDelegate> )delegate
  113. {
  114. [self yb_getRangesWithStrings:strings];
  115. self.userInteractionEnabled = YES;
  116. if (self.delegate != delegate) {
  117. self.delegate = delegate;
  118. }
  119. }
  120. - (void)yb_addAttributeTapActionWithRanges:(NSArray<NSString *> *)ranges tapClicked:(void (^)(UILabel *, NSString *, NSRange, NSInteger))tapClick
  121. {
  122. [self yb_getRangesWithRanges:ranges];
  123. self.userInteractionEnabled = YES;
  124. if (self.tapBlock != tapClick) {
  125. self.tapBlock = tapClick;
  126. }
  127. }
  128. - (void)yb_addAttributeTapActionWithRanges:(NSArray<NSString *> *)ranges delegate:(id<YBAttributeTapActionDelegate>)delegate
  129. {
  130. [self yb_getRangesWithRanges:ranges];
  131. self.userInteractionEnabled = YES;
  132. if (self.delegate != delegate) {
  133. self.delegate = delegate;
  134. }
  135. }
  136. #pragma mark - touchAction
  137. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  138. {
  139. if (!self.isTapAction) {
  140. [super touchesBegan:touches withEvent:event];
  141. return;
  142. }
  143. if (objc_getAssociatedObject(self, @selector(enabledTapEffect))) {
  144. self.isTapEffect = self.enabledTapEffect;
  145. }
  146. UITouch *touch = [touches anyObject];
  147. CGPoint point = [touch locationInView:self];
  148. __weak typeof(self) weakSelf = self;
  149. BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) {
  150. if (weakSelf.isTapEffect) {
  151. [weakSelf yb_saveEffectDicWithRange:range];
  152. [weakSelf yb_tapEffectWithStatus:YES];
  153. }
  154. }];
  155. if (!ret) {
  156. [super touchesBegan:touches withEvent:event];
  157. }
  158. }
  159. - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  160. {
  161. if (!self.isTapAction) {
  162. [super touchesEnded:touches withEvent:event];
  163. return;
  164. }
  165. if (self.isTapEffect) {
  166. [self performSelectorOnMainThread:@selector(yb_tapEffectWithStatus:) withObject:nil waitUntilDone:NO];
  167. }
  168. UITouch *touch = [touches anyObject];
  169. CGPoint point = [touch locationInView:self];
  170. __weak typeof(self) weakSelf = self;
  171. BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) {
  172. if (weakSelf.tapBlock) {
  173. weakSelf.tapBlock (weakSelf, string, range, index);
  174. }
  175. if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(yb_tapAttributeInLabel:string:range:index:)]) {
  176. [weakSelf.delegate yb_tapAttributeInLabel:weakSelf string:string range:range index:index];
  177. }
  178. }];
  179. if (!ret) {
  180. [super touchesEnded:touches withEvent:event];
  181. }
  182. }
  183. - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  184. {
  185. if (!self.isTapAction) {
  186. [super touchesCancelled:touches withEvent:event];
  187. return;
  188. }
  189. if (self.isTapEffect) {
  190. [self performSelectorOnMainThread:@selector(yb_tapEffectWithStatus:) withObject:nil waitUntilDone:NO];
  191. }
  192. UITouch *touch = [touches anyObject];
  193. CGPoint point = [touch locationInView:self];
  194. __weak typeof(self) weakSelf = self;
  195. BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) {
  196. if (weakSelf.tapBlock) {
  197. weakSelf.tapBlock (weakSelf, string, range, index);
  198. }
  199. if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(yb_tapAttributeInLabel:string:range:index:)]) {
  200. [weakSelf.delegate yb_tapAttributeInLabel:weakSelf string:string range:range index:index];
  201. }
  202. }];
  203. if (!ret) {
  204. [super touchesCancelled:touches withEvent:event];
  205. }
  206. }
  207. #pragma mark - getTapFrame
  208. - (BOOL)yb_getTapFrameWithTouchPoint:(CGPoint)point result:(void (^) (NSString *string , NSRange range , NSInteger index))resultBlock
  209. {
  210. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedText);
  211. CGMutablePathRef Path = CGPathCreateMutable();
  212. CGPathAddRect(Path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height + 20));
  213. CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), Path, NULL);
  214. CFArrayRef lines = CTFrameGetLines(frame);
  215. CGFloat total_height = [self yb_textSizeWithAttributedString:self.attributedText width:self.bounds.size.width numberOfLines:0].height;
  216. if (!lines) {
  217. CFRelease(frame);
  218. CFRelease(framesetter);
  219. CGPathRelease(Path);
  220. return NO;
  221. }
  222. CFIndex count = CFArrayGetCount(lines);
  223. CGPoint origins[count];
  224. CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
  225. CGAffineTransform transform = [self yb_transformForCoreText];
  226. for (CFIndex i = 0; i < count; i++) {
  227. CGPoint linePoint = origins[i];
  228. CTLineRef line = CFArrayGetValueAtIndex(lines, i);
  229. CGRect flippedRect = [self yb_getLineBounds:line point:linePoint];
  230. CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
  231. CGFloat lineOutSpace = (self.bounds.size.height - total_height) / 2;
  232. rect.origin.y = lineOutSpace + [self yb_getLineOrign:line];
  233. if (self.enlargeTapArea) {
  234. rect.origin.y -= 5;
  235. rect.size.height += 10;
  236. }
  237. if (CGRectContainsPoint(rect, point)) {
  238. CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
  239. CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint);
  240. CGFloat offset;
  241. CTLineGetOffsetForStringIndex(line, index, &offset);
  242. if (offset > relativePoint.x) {
  243. index = index - 1;
  244. }
  245. NSInteger link_count = self.attributeStrings.count;
  246. for (int j = 0; j < link_count; j++) {
  247. YBAttributeModel *model = self.attributeStrings[j];
  248. NSRange link_range = model.range;
  249. if (NSLocationInRange(index, link_range)) {
  250. if (resultBlock) {
  251. resultBlock (model.str , model.range , (NSInteger)j);
  252. }
  253. CFRelease(frame);
  254. CFRelease(framesetter);
  255. CGPathRelease(Path);
  256. return YES;
  257. }
  258. }
  259. }
  260. }
  261. CFRelease(frame);
  262. CFRelease(framesetter);
  263. CGPathRelease(Path);
  264. return NO;
  265. }
  266. - (CGAffineTransform)yb_transformForCoreText
  267. {
  268. return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f);
  269. }
  270. - (CGRect)yb_getLineBounds:(CTLineRef)line point:(CGPoint)point
  271. {
  272. CGFloat ascent = 0.0f;
  273. CGFloat descent = 0.0f;
  274. CGFloat leading = 0.0f;
  275. CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  276. CGFloat height = 0.0f;
  277. CFRange range = CTLineGetStringRange(line);
  278. NSAttributedString * attributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(range.location, range.length)];
  279. if ([attributedString.string hasSuffix:@"\n"] && attributedString.string.length > 1) {
  280. attributedString = [attributedString attributedSubstringFromRange:NSMakeRange(0, attributedString.length - 1)];
  281. }
  282. height = [self yb_textSizeWithAttributedString:attributedString width:self.bounds.size.width numberOfLines:0].height;
  283. return CGRectMake(point.x, point.y , width, height);
  284. }
  285. - (CGFloat)yb_getLineOrign:(CTLineRef)line
  286. {
  287. CFRange range = CTLineGetStringRange(line);
  288. if (range.location == 0) {
  289. return 0.;
  290. }else {
  291. NSAttributedString * attributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(0, range.location)];
  292. if ([attributedString.string hasSuffix:@"\n"] && attributedString.string.length > 1) {
  293. attributedString = [attributedString attributedSubstringFromRange:NSMakeRange(0, attributedString.length - 1)];
  294. }
  295. return [self yb_textSizeWithAttributedString:attributedString width:self.bounds.size.width numberOfLines:0].height;
  296. }
  297. }
  298. - (CGSize)yb_textSizeWithAttributedString:(NSAttributedString *)attributedString width:(float)width numberOfLines:(NSInteger)numberOfLines
  299. {
  300. @autoreleasepool {
  301. UILabel *sizeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  302. sizeLabel.numberOfLines = numberOfLines;
  303. sizeLabel.attributedText = attributedString;
  304. CGSize fitSize = [sizeLabel sizeThatFits:CGSizeMake(width, MAXFLOAT)];
  305. return fitSize;
  306. }
  307. }
  308. #pragma mark - tapEffect
  309. - (void)yb_tapEffectWithStatus:(BOOL)status
  310. {
  311. if (self.isTapEffect) {
  312. NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
  313. NSMutableAttributedString *subAtt = [[NSMutableAttributedString alloc] initWithAttributedString:[[self.effectDic allValues] firstObject]];
  314. NSRange range = NSRangeFromString([[self.effectDic allKeys] firstObject]);
  315. if (status) {
  316. [subAtt addAttribute:NSBackgroundColorAttributeName value:self.tapHighlightedColor range:NSMakeRange(0, subAtt.string.length)];
  317. [attStr replaceCharactersInRange:range withAttributedString:subAtt];
  318. }else {
  319. [attStr replaceCharactersInRange:range withAttributedString:subAtt];
  320. }
  321. self.attributedText = attStr;
  322. }
  323. }
  324. - (void)yb_saveEffectDicWithRange:(NSRange)range
  325. {
  326. self.effectDic = [NSMutableDictionary dictionary];
  327. NSAttributedString *subAttribute = [self.attributedText attributedSubstringFromRange:range];
  328. [self.effectDic setObject:subAttribute forKey:NSStringFromRange(range)];
  329. }
  330. #pragma mark - getRange
  331. - (void)yb_getRangesWithStrings:(NSArray <NSString *> *)strings
  332. {
  333. if (self.attributedText == nil) {
  334. self.isTapAction = NO;
  335. return;
  336. }
  337. self.isTapAction = YES;
  338. self.isTapEffect = YES;
  339. __block NSString *totalStr = self.attributedText.string;
  340. self.attributeStrings = [NSMutableArray array];
  341. __weak typeof(self) weakSelf = self;
  342. [strings enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  343. NSRange range = [totalStr rangeOfString:obj];
  344. if (range.length != 0) {
  345. totalStr = [totalStr stringByReplacingCharactersInRange:range withString:[weakSelf yb_getStringWithRange:range]];
  346. YBAttributeModel *model = [YBAttributeModel new];
  347. model.range = range;
  348. model.str = obj;
  349. [weakSelf.attributeStrings addObject:model];
  350. }
  351. }];
  352. }
  353. - (void)yb_getRangesWithRanges:(NSArray <NSString *> *)ranges
  354. {
  355. if (self.attributedText == nil) {
  356. self.isTapAction = NO;
  357. return;
  358. }
  359. self.isTapAction = YES;
  360. self.isTapEffect = YES;
  361. __block NSString *totalStr = self.attributedText.string;
  362. self.attributeStrings = [NSMutableArray array];
  363. __weak typeof(self) weakSelf = self;
  364. [ranges enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  365. NSRange range = NSRangeFromString(obj);
  366. NSAssert(totalStr.length >= range.location + range.length, @"NSRange(%ld,%ld) is out of bounds",range.location,range.length);
  367. NSString * string = [totalStr substringWithRange:range];
  368. YBAttributeModel *model = [YBAttributeModel new];
  369. model.range = range;
  370. model.str = string;
  371. [weakSelf.attributeStrings addObject:model];
  372. }];
  373. }
  374. - (NSString *)yb_getStringWithRange:(NSRange)range
  375. {
  376. NSMutableString *string = [NSMutableString string];
  377. for (int i = 0; i < range.length ; i++) {
  378. [string appendString:@" "];
  379. }
  380. return string;
  381. }
  382. #pragma mark - KVO
  383. - (void)yb_addObserver
  384. {
  385. [self addObserver:self forKeyPath:@"attributedText" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
  386. }
  387. - (void)yb_removeObserver
  388. {
  389. id info = self.observationInfo;
  390. NSString * key = @"attributedText";
  391. NSArray *array = [info valueForKey:@"_observances"];
  392. for (id objc in array) {
  393. id Properties = [objc valueForKeyPath:@"_property"];
  394. NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
  395. if ([key isEqualToString:keyPath]) {
  396. [self removeObserver:self forKeyPath:@"attributedText" context:nil];
  397. }
  398. }
  399. }
  400. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  401. {
  402. if ([keyPath isEqualToString:@"attributedText"]) {
  403. if (self.isTapAction) {
  404. if (![change[NSKeyValueChangeNewKey] isEqual: change[NSKeyValueChangeOldKey]]) {
  405. }
  406. }
  407. }
  408. }
  409. @end