iOS 可点击文本实现方案

作者: 9a957efaf40a | 来源:发表于2019-10-12 11:39 被阅读0次

    需求:实现label部分文字点击,如下图

    WeChatd75f411816a3283acba36fb3138bb914.png
    要求《业务委托书》《个人信息采集及征信查询授权书》两部分可以点击,其他不能点击。

    最容易实现的是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
    

    具体的思路:

    1. - (void)addTarget:(id)target selector:(SEL)sel range:(NSRange)range给指定range添加事件;
    2. - (CGRect)boundingRectForCharacterRange:(NSRange)range获取指定range的范围。这个方法在range是同一行时没有问题,但是如果链接刚好换行,形成多行,那么此时rect获取不正确;
    3. 为了解决第二步的问题,基本思路是判断一个range是否被换行。如果换行,那么将range按照行来截断,给每一段分别再次添加同一个事件;
    4. 使用coreText获取每一行文本的range;
    5. 根据点击范围来进行判断,数组ranges记录了每个range对应的sel。如果相交则调用[self.target performSelector:sel];

    例子中一个UILabeltarget只能是同一个对象,你可以进行改造,使之适用于自己的业务逻辑。

    2019-11-26补充:

    如果使用最后一种label实现方案,此时需要注意:
    如果外部可能给label设置了各种属性,比如对齐方式,文本截断方式等等,那么在- (NSArray *)lines方法和- (CGRect)boundingRectForCharacterRange:(NSRange)range方法中,分别给attStrtextStorage设置富文本格式时,一定要和外部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)];
    

    相关文章

      网友评论

        本文标题:iOS 可点击文本实现方案

        本文链接:https://www.haomeiwen.com/subject/noglyctx.html