#import <UIKit/UIKit.h> typedef void(^LinkTap)(NSRange range, NSURL* url); @interface LinkLabel : UIView @property (nonatomic, copy) NSString* text; @property (nonatomic, strong) UIColor* textColor; @property (nonatomic, assign) CGFloat fontSize; @property (nonatomic, copy) LinkTap tapLink; - (void)addLinkRange:(NSRange)range withUrl:(NSURL *)url; @end
#import "LinkLabel.h" #import <CoreText/CoreText.h> @interface LinkLabel () { CTFrameRef _textFrame; NSMutableArray* _lineRects; NSMutableDictionary* _linkDic;// 存儲連接文本及其range位置 } @end @implementation LinkLabel - (instancetype)init { self = [super init]; if (self) { [self commontInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commontInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commontInit]; } return self; } - (void)commontInit { UIGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGesture:)]; [self addGestureRecognizer:tapRecognizer]; self.userInteractionEnabled = YES; _lineRects = [NSMutableArray array]; _linkDic = [NSMutableDictionary dictionary]; } - (void)addLinkRange:(NSRange)range withUrl:(NSURL *)url { [_linkDic setObject:url forKey:[NSValue valueWithRange:range]]; } // 正則表達式匹配超連接文本 - (void)matchLinkString:(NSString *)string { NSError *error; NSString *regulaStr = @"((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)"; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regulaStr options:NSRegularExpressionCaseInsensitive error:&error]; NSArray *arrayOfAllMatches = [regex matchesInString:string options:0 range:NSMakeRange(0, [string length])]; for (NSTextCheckingResult *match in arrayOfAllMatches) { NSString* linkStrForMatch = [string substringWithRange:match.range]; [_linkDic setObject:[NSURL URLWithString:linkStrForMatch] forKey:[NSValue valueWithRange:match.range]]; } } - (void)setText:(NSString *)text { _text = text; [self matchLinkString:text]; } - (void)drawRect:(CGRect)rect { [super drawRect:rect]; NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:_text]; UIFont* font = [UIFont systemFontOfSize:_fontSize]; [attString addAttribute:(id)kCTFontAttributeName value:font range:NSMakeRange(0, attString.length)]; if (_linkDic.count) { [_linkDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { NSRange range = [key rangeValue]; [attString addAttribute:(id)kCTForegroundColorAttributeName value:(id)[UIColor blueColor].CGColor range:range]; }]; } CGContextRef context = UIGraphicsGetCurrentContext(); // 翻轉座標系 CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, self.bounds); CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, NULL); CTFrameDraw(frame, context); // CFRelease(frame); CFArrayRef lines = CTFrameGetLines(frame); CFIndex count = CFArrayGetCount(lines); // 得到每一行的 origin 座標 CGPoint origins[count]; CTFrameGetLineOrigins(frame, CFRangeMake(0,0), origins); // 翻轉座標系 CGAffineTransform transform = CGAffineTransformMakeTranslation(0, self.bounds.size.height); transform = CGAffineTransformScale(transform, 1.f, -1.f); for (int i = 0; i < count; i++) { CGPoint linePoint = origins[i]; CTLineRef line = CFArrayGetValueAtIndex(lines, i); // 得到每一行的 CGRect 信息 CGRect flippedRect = [self getLineBounds:line point:linePoint]; CGRect rect = CGRectApplyAffineTransform(flippedRect, transform); [_lineRects addObject:[NSValue valueWithCGRect:rect]]; } _textFrame = frame; CFRelease(path); CFRelease(framesetter); } - (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point { CGFloat ascent = 0.0f; CGFloat descent = 0.0f; CGFloat leading = 0.0f; //ascent是文本上線,descent是文本下線,leading是文本起始線 CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading); CGFloat height = ascent + descent; return CGRectMake(point.x, point.y - descent, width, height); } - (void)userTapGesture:(UIGestureRecognizer *)recognizer { CGPoint point = [recognizer locationInView:self]; CFArrayRef lines = CTFrameGetLines(_textFrame); CFIndex count = CFArrayGetCount(lines); for (int i = 0; i < count; i ++) { CTLineRef line = CFArrayGetValueAtIndex(lines, i); CGRect rect = [_lineRects[i] CGRectValue]; if (CGRectContainsPoint(rect, point)) { // 將點擊的座標轉換成相對於當前行的座標 CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect)); // 得到當前點擊座標對應的字符串偏移 // 點擊座標最近的光標的最近字符的index,點擊字符前半部分會定位到上一個字符的index,點擊後半部分才獲得正確的index CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint); CGFloat glyphStart; // 根據index獲取對應字符據行初始位置距離 CTLineGetOffsetForStringIndex(line, idx, &glyphStart); if (relativePoint.x < glyphStart && idx) { --idx; } [self matchAndOpenLinkUrl:idx]; break; } } } - (void)matchAndOpenLinkUrl:(CFIndex)index { [_linkDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { NSRange range = [key rangeValue]; if (NSLocationInRange(index, range)) { NSURL* url = (NSURL *)obj; if (self.tapLink) { self.tapLink(range, url); } *stop = YES; } }]; } @end
xib上使用例子:正則表達式
@property (weak, nonatomic) IBOutlet LinkLabel *linkLabel;
_linkLabel.text = @"陷陣@86之志有死無生http://ddd.edu,一點寒芒先到,隨後槍出如龍。縱使敵衆我寡,末將亦能萬軍從中取敵將首級www.baidu.com"; _linkLabel.fontSize = 17; [_linkLabel addLinkRange:NSMakeRange(2, 3) withUrl:[NSURL URLWithString:@"www.google.com"]]; _linkLabel.tapLink = ^(NSRange range, NSURL* url) { NSLog(@"url:%@", url.absoluteString); };