// // UILabel+YBAttributeTextTapAction.m // // Created by LYB on 16/7/1. // Copyright © 2016年 LYB. All rights reserved. // #import "UILabel+YBAttributeTextTapAction.h" #import #import #import @interface YBAttributeModel : NSObject @property (nonatomic, copy) NSString *str; @property (nonatomic) NSRange range; @end @implementation YBAttributeModel @end @implementation UILabel (YBAttributeTextTapAction) #pragma mark - AssociatedObjects - (NSMutableArray *)attributeStrings { return objc_getAssociatedObject(self, _cmd); } - (void)setAttributeStrings:(NSMutableArray *)attributeStrings { objc_setAssociatedObject(self, @selector(attributeStrings), attributeStrings, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSMutableDictionary *)effectDic { return objc_getAssociatedObject(self, _cmd); } - (void)setEffectDic:(NSMutableDictionary *)effectDic { objc_setAssociatedObject(self, @selector(effectDic), effectDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isTapAction { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsTapAction:(BOOL)isTapAction { objc_setAssociatedObject(self, @selector(isTapAction), @(isTapAction), OBJC_ASSOCIATION_ASSIGN); } - (void (^)(UILabel *, NSString *, NSRange, NSInteger))tapBlock { return objc_getAssociatedObject(self, _cmd); } - (void)setTapBlock:(void (^)(UILabel *, NSString *, NSRange, NSInteger))tapBlock { objc_setAssociatedObject(self, @selector(tapBlock), tapBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (id)delegate { return objc_getAssociatedObject(self, _cmd); } - (void)setDelegate:(id)delegate { objc_setAssociatedObject(self, @selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN); } - (BOOL)enabledTapEffect { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setEnabledTapEffect:(BOOL)enabledTapEffect { objc_setAssociatedObject(self, @selector(enabledTapEffect), @(enabledTapEffect), OBJC_ASSOCIATION_ASSIGN); self.isTapEffect = enabledTapEffect; } - (BOOL)enlargeTapArea { NSNumber * number = objc_getAssociatedObject(self, _cmd); if (!number) { number = @(YES); objc_setAssociatedObject(self, _cmd, number, OBJC_ASSOCIATION_ASSIGN); } return [number boolValue]; } - (void)setEnlargeTapArea:(BOOL)enlargeTapArea { objc_setAssociatedObject(self, @selector(enlargeTapArea), @(enlargeTapArea), OBJC_ASSOCIATION_ASSIGN); } - (UIColor *)tapHighlightedColor { UIColor * color = objc_getAssociatedObject(self, _cmd); if (!color) { color = [UIColor lightGrayColor]; objc_setAssociatedObject(self, _cmd, color, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return color; } - (void)setTapHighlightedColor:(UIColor *)tapHighlightedColor { objc_setAssociatedObject(self, @selector(tapHighlightedColor), tapHighlightedColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isTapEffect { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsTapEffect:(BOOL)isTapEffect { objc_setAssociatedObject(self, @selector(isTapEffect), @(isTapEffect), OBJC_ASSOCIATION_ASSIGN); } #pragma mark - mainFunction - (void)yb_addAttributeTapActionWithStrings:(NSArray *)strings tapClicked:(void (^) (UILabel * label, NSString *string, NSRange range, NSInteger index))tapClick { [self yb_removeAttributeTapActions]; [self yb_getRangesWithStrings:strings]; self.userInteractionEnabled = YES; if (self.tapBlock != tapClick) { self.tapBlock = tapClick; } } - (void)yb_addAttributeTapActionWithStrings:(NSArray *)strings delegate:(id )delegate { [self yb_removeAttributeTapActions]; [self yb_getRangesWithStrings:strings]; self.userInteractionEnabled = YES; if (self.delegate != delegate) { self.delegate = delegate; } } - (void)yb_addAttributeTapActionWithRanges:(NSArray *)ranges tapClicked:(void (^)(UILabel *, NSString *, NSRange, NSInteger))tapClick { [self yb_removeAttributeTapActions]; [self yb_getRangesWithRanges:ranges]; self.userInteractionEnabled = YES; if (self.tapBlock != tapClick) { self.tapBlock = tapClick; } } - (void)yb_addAttributeTapActionWithRanges:(NSArray *)ranges delegate:(id)delegate { [self yb_removeAttributeTapActions]; [self yb_getRangesWithRanges:ranges]; self.userInteractionEnabled = YES; if (self.delegate != delegate) { self.delegate = delegate; } } - (void)yb_removeAttributeTapActions { self.tapBlock = nil; self.delegate = nil; self.effectDic = nil; self.isTapAction = NO; self.attributeStrings = [NSMutableArray array]; } #pragma mark - touchAction - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (!self.isTapAction) { [super touchesBegan:touches withEvent:event]; return; } if (objc_getAssociatedObject(self, @selector(enabledTapEffect))) { self.isTapEffect = self.enabledTapEffect; } UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; __weak typeof(self) weakSelf = self; BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) { if (weakSelf.isTapEffect) { [weakSelf yb_saveEffectDicWithRange:range]; [weakSelf yb_tapEffectWithStatus:YES]; } }]; if (!ret) { [super touchesBegan:touches withEvent:event]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (!self.isTapAction) { [super touchesEnded:touches withEvent:event]; return; } if (self.isTapEffect) { [self performSelectorOnMainThread:@selector(yb_tapEffectWithStatus:) withObject:nil waitUntilDone:NO]; } UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; __weak typeof(self) weakSelf = self; BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) { if (weakSelf.tapBlock) { weakSelf.tapBlock (weakSelf, string, range, index); } if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(yb_tapAttributeInLabel:string:range:index:)]) { [weakSelf.delegate yb_tapAttributeInLabel:weakSelf string:string range:range index:index]; } }]; if (!ret) { [super touchesEnded:touches withEvent:event]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { if (!self.isTapAction) { [super touchesCancelled:touches withEvent:event]; return; } if (self.isTapEffect) { [self performSelectorOnMainThread:@selector(yb_tapEffectWithStatus:) withObject:nil waitUntilDone:NO]; } UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; __weak typeof(self) weakSelf = self; BOOL ret = [self yb_getTapFrameWithTouchPoint:point result:^(NSString *string, NSRange range, NSInteger index) { if (weakSelf.tapBlock) { weakSelf.tapBlock (weakSelf, string, range, index); } if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(yb_tapAttributeInLabel:string:range:index:)]) { [weakSelf.delegate yb_tapAttributeInLabel:weakSelf string:string range:range index:index]; } }]; if (!ret) { [super touchesCancelled:touches withEvent:event]; } } #pragma mark - getTapFrame - (BOOL)yb_getTapFrameWithTouchPoint:(CGPoint)point result:(void (^) (NSString *string , NSRange range , NSInteger index))resultBlock { CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedText); CGMutablePathRef Path = CGPathCreateMutable(); CGPathAddRect(Path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height + 20)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), Path, NULL); CFArrayRef lines = CTFrameGetLines(frame); CGFloat total_height = [self yb_textSizeWithAttributedString:self.attributedText width:self.bounds.size.width numberOfLines:0].height; if (!lines) { CFRelease(frame); CFRelease(framesetter); CGPathRelease(Path); return NO; } CFIndex count = CFArrayGetCount(lines); CGPoint origins[count]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); CGAffineTransform transform = [self yb_transformForCoreText]; for (CFIndex i = 0; i < count; i++) { CGPoint linePoint = origins[i]; CTLineRef line = CFArrayGetValueAtIndex(lines, i); CGRect flippedRect = [self yb_getLineBounds:line point:linePoint]; CGRect rect = CGRectApplyAffineTransform(flippedRect, transform); CGFloat lineOutSpace = (self.bounds.size.height - total_height) / 2; rect.origin.y = lineOutSpace + [self yb_getLineOrign:line]; if (self.enlargeTapArea) { rect.origin.y -= 5; rect.size.height += 10; } if (CGRectContainsPoint(rect, point)) { CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect)); CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint); CGFloat offset; CTLineGetOffsetForStringIndex(line, index, &offset); if (offset > relativePoint.x) { index = index - 1; } NSInteger link_count = self.attributeStrings.count; for (int j = 0; j < link_count; j++) { YBAttributeModel *model = self.attributeStrings[j]; NSRange link_range = model.range; if (NSLocationInRange(index, link_range)) { if (resultBlock) { resultBlock (model.str , model.range , (NSInteger)j); } CFRelease(frame); CFRelease(framesetter); CGPathRelease(Path); return YES; } } } } CFRelease(frame); CFRelease(framesetter); CGPathRelease(Path); return NO; } - (CGAffineTransform)yb_transformForCoreText { return CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f); } - (CGRect)yb_getLineBounds:(CTLineRef)line point:(CGPoint)point { CGFloat ascent = 0.0f; CGFloat descent = 0.0f; CGFloat leading = 0.0f; CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading); CGFloat height = 0.0f; CFRange range = CTLineGetStringRange(line); NSAttributedString * attributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(range.location, range.length)]; if ([attributedString.string hasSuffix:@"\n"] && attributedString.string.length > 1) { attributedString = [attributedString attributedSubstringFromRange:NSMakeRange(0, attributedString.length - 1)]; } height = [self yb_textSizeWithAttributedString:attributedString width:self.bounds.size.width numberOfLines:0].height; return CGRectMake(point.x, point.y , width, height); } - (CGFloat)yb_getLineOrign:(CTLineRef)line { CFRange range = CTLineGetStringRange(line); if (range.location == 0) { return 0.; }else { NSAttributedString * attributedString = [self.attributedText attributedSubstringFromRange:NSMakeRange(0, range.location)]; if ([attributedString.string hasSuffix:@"\n"] && attributedString.string.length > 1) { attributedString = [attributedString attributedSubstringFromRange:NSMakeRange(0, attributedString.length - 1)]; } return [self yb_textSizeWithAttributedString:attributedString width:self.bounds.size.width numberOfLines:0].height; } } - (CGSize)yb_textSizeWithAttributedString:(NSAttributedString *)attributedString width:(float)width numberOfLines:(NSInteger)numberOfLines { @autoreleasepool { UILabel *sizeLabel = [[UILabel alloc] initWithFrame:CGRectZero]; sizeLabel.numberOfLines = numberOfLines; sizeLabel.attributedText = attributedString; CGSize fitSize = [sizeLabel sizeThatFits:CGSizeMake(width, MAXFLOAT)]; return fitSize; } } #pragma mark - tapEffect - (void)yb_tapEffectWithStatus:(BOOL)status { if (self.isTapEffect) { NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; NSMutableAttributedString *subAtt = [[NSMutableAttributedString alloc] initWithAttributedString:[[self.effectDic allValues] firstObject]]; NSRange range = NSRangeFromString([[self.effectDic allKeys] firstObject]); if (status) { [subAtt addAttribute:NSBackgroundColorAttributeName value:self.tapHighlightedColor range:NSMakeRange(0, subAtt.string.length)]; [attStr replaceCharactersInRange:range withAttributedString:subAtt]; }else { [attStr replaceCharactersInRange:range withAttributedString:subAtt]; } self.attributedText = attStr; } } - (void)yb_saveEffectDicWithRange:(NSRange)range { self.effectDic = [NSMutableDictionary dictionary]; NSAttributedString *subAttribute = [self.attributedText attributedSubstringFromRange:range]; [self.effectDic setObject:subAttribute forKey:NSStringFromRange(range)]; } #pragma mark - getRange - (void)yb_getRangesWithStrings:(NSArray *)strings { if (self.attributedText == nil) { self.isTapAction = NO; return; } self.isTapAction = YES; self.isTapEffect = YES; __block NSString *totalStr = self.attributedText.string; self.attributeStrings = [NSMutableArray array]; __weak typeof(self) weakSelf = self; [strings enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSRange range = [totalStr rangeOfString:obj]; if (range.length != 0) { totalStr = [totalStr stringByReplacingCharactersInRange:range withString:[weakSelf yb_getStringWithRange:range]]; YBAttributeModel *model = [YBAttributeModel new]; model.range = range; model.str = obj; [weakSelf.attributeStrings addObject:model]; } }]; } - (void)yb_getRangesWithRanges:(NSArray *)ranges { if (self.attributedText == nil) { self.isTapAction = NO; return; } self.isTapAction = YES; self.isTapEffect = YES; __block NSString *totalStr = self.attributedText.string; self.attributeStrings = [NSMutableArray array]; __weak typeof(self) weakSelf = self; [ranges enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSRange range = NSRangeFromString(obj); NSAssert(totalStr.length >= range.location + range.length, @"NSRange(%ld,%ld) is out of bounds",range.location,range.length); NSString * string = [totalStr substringWithRange:range]; YBAttributeModel *model = [YBAttributeModel new]; model.range = range; model.str = string; [weakSelf.attributeStrings addObject:model]; }]; } - (NSString *)yb_getStringWithRange:(NSRange)range { NSMutableString *string = [NSMutableString string]; for (int i = 0; i < range.length ; i++) { [string appendString:@" "]; } return string; } #pragma mark - KVO - (void)yb_addObserver { [self addObserver:self forKeyPath:@"attributedText" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil]; } - (void)yb_removeObserver { id info = self.observationInfo; NSString * key = @"attributedText"; NSArray *array = [info valueForKey:@"_observances"]; for (id objc in array) { id Properties = [objc valueForKeyPath:@"_property"]; NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"]; if ([key isEqualToString:keyPath]) { [self removeObserver:self forKeyPath:@"attributedText" context:nil]; } } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"attributedText"]) { if (self.isTapAction) { if (![change[NSKeyValueChangeNewKey] isEqual: change[NSKeyValueChangeOldKey]]) { } } } } @end