iOS 可点击文本实现方案
2019-10-12 本文已影响0人
9a957efaf40a
需求:实现label部分文字点击,如下图
要求《业务委托书》和《个人信息采集及征信查询授权书》两部分可以点击,其他不能点击。
最容易实现的是UITextView
,UITextView
有三种实现方法。
1.使用属性字符串
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"本人确认阅读并同意签署《业务委托书》及《个人信息采集及征信查询授权书》";
NSMutableAttributedString *attribute = [[NSMutableAttributedString alloc] initWithString:str];
[attribute addAttribute:NSLinkAttributeName value:@"labelAction://type1" range:[str rangeOfString:@"《业务委托书》"]];
[attribute addAttribute:NSLinkAttributeName value:@"labelAction://type2" range:[str rangeOfString:@"《个人信息采集及征信查询授权书》"]];
self.textView.attributedText = attribute;
self.textView.delegate = self;
self.textView.linkTextAttributes = @{NSForegroundColorAttributeName:[UIColor colorWithRed:1 green:0 blue:0 alpha:1],NSFontAttributeName:[UIFont systemFontOfSize:30]};
self.textView.editable = NO;
}
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange interaction:(UITextItemInteraction)interaction {
NSLog(@"%@",URL);
return YES;
}
效果图:
WeChat5935df9f8d91d48380e6bbab23237c98.png
点击效果:
2019-09-20 17:11:45.895663+0800 文本文字点击[13700:5109072] labelaction://type1
代理方法iOS10几以后才可以使用,解决思路是自定义URL Types,将它作为自定义链接,可以在AppDelegate中的openURL:方法中截获。
这种方式有几个问题:
- 无法给textView设置属性(尝试基本的font和attributeFont都没有效果,如果你知道怎么解决,欢迎在下方留言)
- 长按链接会弹出系统的actionSheet(尝试很多手段,无法禁用)
2.使用HTML链接
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *html = @"<body style=\"color: darkgray; font-size: 15px;\">本人确认阅读并同意签署<a href='labelAction://type1'>《业务委托书》</a>及<a href='labelAction://type2'>《个人信息采集及征信查询授权书》</a></body>";
NSData *htmlData = [html dataUsingEncoding:NSUnicodeStringEncoding];
NSAttributedString *attribute = [[NSAttributedString alloc] initWithData:htmlData options:@{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType} documentAttributes:NULL error:nil];
self.textView.attributedText = attribute;
self.textView.delegate = self;
self.textView.linkTextAttributes = @{NSForegroundColorAttributeName:[UIColor colorWithRed:1 green:0 blue:0 alpha:1]};
self.textView.editable = NO;
}
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange interaction:(UITextItemInteraction)interaction {
NSLog(@"%@",URL);
return YES;
}
效果图:
WeChat9ca083213425b6c0488ec6f7c4aea775.png
点击效果:
2019-09-20 17:13:45.895663+0800 文本文字点击[13700:5109072] labelaction://type1
和第一种一样,依靠自定义链接或代理方法截获。
这种方式有几个问题:
- 富文本属性得设置在html中;
- 长按链接会弹出系统的actionSheet。
3.使用系统API来获取指定文本的rect,当触摸事件发生时,判断点击区域是否和文本的区域重叠
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"本人确认阅读并同意签署《业务委托书》及《个人信息采集及征信查询授权书》";
self.textView.text = str;
NSRange range = [str rangeOfString:@"《业务委托书》"];
self.textView.selectedRange = range;
UITextRange *textRange = self.textView.selectedTextRange;
NSArray <UITextSelectionRect *>*rects = [self.textView selectionRectsForRange:textRange];
for (UITextSelectionRect *selectionRect in rects) {
NSLog(@"%@",NSStringFromCGRect(selectionRect.rect));
UIView *view = [[UIView alloc] initWithFrame:selectionRect.rect];
view.backgroundColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:0.3];
[self.textView insertSubview:view atIndex:0];
}
}
效果图:
WeChat94116b22e21dccc2896ab8410d43b17f.png
在该方法中,只做了匹配第一个字符串,第二个同理。
此时,可以根据touchBegan方法的点来判断是否被包含在匹配的rect中,从而回调相应事件。
实际上,使用UILabel也可以实现(须借助coreText框架)
使用UILabel,和UITextView的第三种思路一样,获取点击字符串的rect,判断点击范围是否在rect中。
代码如下:
#import "UILabel+JHTapLabel.h"
#import <CoreText/CoreText.h>
@interface UILabel ()
@property (nonatomic, strong) NSMutableArray *ranges;
@property (nonatomic, weak) id target;
@end
@implementation UILabel (JHTapLabel)
- (void)setRanges:(NSMutableArray *)ranges {
objc_setAssociatedObject(self, @selector(ranges), ranges, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSMutableArray *)ranges {
return objc_getAssociatedObject(self, @selector(ranges));
}
- (void)setTarget:(id)target {
objc_setAssociatedObject(self, @selector(target), target, OBJC_ASSOCIATION_ASSIGN);
}
- (id)target {
return objc_getAssociatedObject(self, @selector(target));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
for (NSDictionary *info in self.ranges) {
CGRect rect = [info[@"rect"] CGRectValue];
if (CGRectContainsPoint(rect, point)) {
SEL sel = NSSelectorFromString(info[@"sel"]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:sel];
#pragma clang diagnostic pop
}
}
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
- (void)addTarget:(id)target selector:(SEL)sel range:(NSRange)range {
self.target = target;
if (!self.ranges) {
self.ranges = [NSMutableArray array];
}
self.userInteractionEnabled = YES;
NSArray *lineRanges = [self lines];
NSRange targetRange = range;
for (int i = 0; i < lineRanges.count; i++) {
NSRange lineRange = [lineRanges[i] rangeValue];
NSRange intersectionRange = NSIntersectionRange(targetRange, lineRange);
// 两个range有相交
if (intersectionRange.length != 0) {
// 如果targetRange的范围超出了lineRange
if (NSMaxRange(targetRange) > NSMaxRange(lineRange)) {
[self addTarget:target selector:sel range:intersectionRange];
[self addTarget:target selector:sel range:NSMakeRange(NSMaxRange(intersectionRange), targetRange.length - intersectionRange.length)];
}else {
CGRect rangeRect = [self boundingRectForCharacterRange:range];
[self.ranges addObject:@{@"sel":NSStringFromSelector(sel),
@"rect":[NSValue valueWithCGRect:rangeRect]
}];
}
/*
一旦有相交,则相交的range如果是多行,会被拆分成多个range。
原始的range就不再使用了,这里直接跳出循环*/
break;
}
}
}
- (CGRect)boundingRectForCharacterRange:(NSRange)range
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];
[textStorage addAttributes:@{NSFontAttributeName:self.font} range:NSMakeRange(0, textStorage.string.length)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(CGRectGetWidth(self.frame), CGFLOAT_MAX)];
textContainer.lineFragmentPadding = 0;
textContainer.lineBreakMode = self.lineBreakMode;
[layoutManager addTextContainer:textContainer];
NSRange glyphRange;
[layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
CGRect rect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
return rect;
}
- (NSArray *)lines {
NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
[attStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, attStr.length)];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attStr);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0,0,CGRectGetWidth(self.frame), 100000));
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
NSMutableArray *linesArray = [[NSMutableArray alloc]init];
for (id line in lines) {
CTLineRef lineRef = (__bridge CTLineRef )line;
CFRange lineRange = CTLineGetStringRange(lineRef);
NSRange range = NSMakeRange(lineRange.location, lineRange.length);
[linesArray addObject:[NSValue valueWithRange:range]];
}
CFRelease(frameSetter);
CFRelease(path);
CFRelease(frame);
return linesArray;
}
@end
具体的思路:
-
- (void)addTarget:(id)target selector:(SEL)sel range:(NSRange)range
给指定range添加事件; -
- (CGRect)boundingRectForCharacterRange:(NSRange)range
获取指定range的范围。这个方法在range是同一行时没有问题,但是如果链接刚好换行,形成多行,那么此时rect获取不正确; - 为了解决第二步的问题,基本思路是判断一个range是否被换行。如果换行,那么将range按照行来截断,给每一段分别再次添加同一个事件;
- 使用coreText获取每一行文本的range;
- 根据点击范围来进行判断,数组ranges记录了每个range对应的sel。如果相交则调用
[self.target performSelector:sel];
例子中一个UILabel
的target
只能是同一个对象,你可以进行改造,使之适用于自己的业务逻辑。
2019-11-26补充:
如果使用最后一种label实现方案,此时需要注意:
如果外部可能给label设置了各种属性,比如对齐方式,文本截断方式等等,那么在- (NSArray *)lines
方法和- (CGRect)boundingRectForCharacterRange:(NSRange)range
方法中,分别给attStr
和textStorage
设置富文本格式时,一定要和外部label设置的匹配,否则可能导致这两个函数计算范围出现误差。
比如在- (NSArray *)lines
方法中,给 attStr
设置font,对齐方式等等:
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = self.lineBreakMode;
style.alignment = self.textAlignment;
[attStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, attStr.length)];
[attStr addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, attStr.length)];
在- (CGRect)boundingRectForCharacterRange:(NSRange)range
方法中同样要给textStorage
设置:
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = self.lineBreakMode;
style.alignment = self.textAlignment;
[textStorage addAttributes:@{NSFontAttributeName:self.font,NSParagraphStyleAttributeName:style} range:NSMakeRange(0, textStorage.string.length)];