[iOS]0 行代码集成 UILabel 字符串匹配

大家都用过微博,微博有用户“@盼盼”,话题“#怎么追漂亮女孩?#”,还有网络链接“http: //...”,还有各种协议“《FBI WARNING...》”,实际项目中要求给这些特殊的字符着色,就像 demo 这样。这篇文章将带你封装这个功能,并且在实际开发中 0 行代码集成。

01. 怎么集成到你的项目?

如果你赶项目没时间看源码,只需要看一下下面的使用就可以了。

  • 下载我的源码(GIT地址),把 JPLabel 拖进你的项目。
  • Xib 或者 SB 中,直接把你的 UILabel 的类别改成 JPLabel,你的 Label 就自动拥有这些功能了。
  • 使用代码创建的时候,你只需要将你的 UILabel 继承自 JPLabel 就自动拥有这些功能了。
  • 你肯定要进行自定义颜色的,因为我定义的颜色基本上没法看(???)。你只需要像下面这些代码一样就可以了。
#import "ViewController.h"
#import "JPLabel.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet JPLabel *textLabel;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    self.textLabel.text = @"#JPLabel# 用于匹配字符串的内容显示, 用户:@盼盼, 包括话题:#怎么追漂亮女孩?#, 链接:https://github.com/Chris-Pan/JPLabel, 协议:《退款政策》";
    
    // 非匹配内容文字颜色
    self.textLabel.jp_commonTextColor = [UIColor colorWithRed:112.0/255 green:93.0/255 blue:77.0/255 alpha:1];

    // 点选高亮文字颜色
    self.textLabel.jp_textHightLightBackgroundColor = [UIColor colorWithRed:237.0/255 green:213.0/255 blue:177.0/255 alpha:1];

    // 匹配文字颜色
    [self.textLabel setHightLightTextColor:[UIColor colorWithRed:132.0/255 green:77.0/255 blue:255.0/255 alpha:1] forHandleStyle:HandleStyleUser];
    [self.textLabel setHightLightTextColor:[UIColor colorWithRed:9.0/255 green:163.0/255 blue:213.0/255 alpha:1] forHandleStyle:HandleStyleLink];
    [self.textLabel setHightLightTextColor:[UIColor colorWithRed:254.0/255 green:156.0/255 blue:59.0/255 alpha:1] forHandleStyle:HandleStyleTopic];
    [self.textLabel setHightLightTextColor:[UIColor colorWithRed:255.0/255 green:69.0/255 blue:0.0/255 alpha:1] forHandleStyle:HandleStyleAgreement];

    // 自定义匹配的文字和颜色
    self.textLabel.jp_matchArr = @[
                                    @{
                                        @"string" : @"高亮显示",
                                        @"color" : [UIColor colorWithRed:0.55 green:0.86 blue:0.34 alpha:1]
                                    }
                                ];
    // 匹配到合适内容的回调
    self.textLabel.jp_tapOperation = ^(UILabel *label, HandleStyle style, NSString *selectedString, NSRange range){
        // 你要做的事
        NSLog(@"%@", selectedString);
    };
}

#pragma mark JPLabelDelegate

-(void)jp_label:(JPLabel *)label didSelectedString:(NSString *)selectedStr forStyle:(HandleStyle)style inRange:(NSRange)range{

    // 你想要做的事
    NSLog(@"代理打印 %@", selectedStr);
}

@end
复制代码

02.涉及的知识点?

1.怎么将结构体保存到数组中?
  • 大家都知道 Objective-C 的数组只能存储继承自 NSObject 的对象。这个功能有一个需求,我们使用正则表达式匹配到的结果是一个 NSRange 的实例对象,我们需要将匹配结果,也就是多个 NSRange 实例缓存起来,因为后期要做点击结果的匹配。使用简单的 "@()" 包装是不能将一个结构体转换成为对象的。
  • 这时,我们需要借助 NSValue 这个类,采取先将 NSRange 实例转换为 NSValue,我们把 NSValue 存在数组当中。
  • 当使用时,先将 NSValue 转换为 NSRange,这样就可以间接的存储我们要的值。 代码如下:
// 存值
NSMutableArray *ranges = [NSMutableArray array];
for (NSTextCheckingResult *result in results) {
    // 将结构体保存到数组
    // 先用一个变量接受结构体
    NSRange range = result.range;
    NSValue *value = [NSValue valueWithBytes:&range objCType:@encode(NSRange)];
    [ranges addObject:value];
}

// 取值
for (NSValue *value in ranges) {
    NSRange range;
    [value getValue:&range];
    ...
}
复制代码
2.正则表达式?
  • 我们的功能是匹配诸如“@...”,“#...#”,“《...》”,“ http:// ”等关键字,并把这些关键字用我们想要的颜色标注出来这样一个功能,当我们去找这些关键字的时候,就需要正则表达式。
  • 在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式 就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
  • objc 自然是集成了正则表达式的,下面我们看看简单的使用正则表达式来匹配出电话号码
let str ="132468842823"

// 1.创建规则
let pattern = "^1[3578][0-9]{9}$"

// 2.创建正则表达式对象 
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
    return
}

// 3.开始匹配
let results = regex.matchesInString(str, options: [], range: NSRange(location: 0, length: str.characters.count))

// 4.遍历匹配结果
for result in results {
    print((str as NSString).substringWithRange(result.range))
}
复制代码
3.实现基础?

先来看一张我从 Text Kit 文档中找出来的图:

这里我简单的描述一下每个类的作用,具体你如果感兴趣,可以自己去看文档,或者看这篇文档的中文翻译文章

  • NSTextStorageNSMutableAttributedString 子类,存储用于显示的文本,同时也管理一组 NSLayoutManager 对象。
  • NSLayoutManager 负责将 NSTextStorage 中的文本显示到 NSTextContainer 区域。它将 Unicode 字符转换成 glyph,并监督 glyphNSTextContainer 对象所定义的区域中布局的过程。
  • NSTextContainer 负责保存文本显示的区域。

03.主体思路 ?

主题思路分为两步走:

  • 第一,先实现文字按照自定义的方式显示。也就是给关键字着色。
  • 第二,实现关键字的点击响应,并拿到用户点击的关键字段。
第一,实现自定义文本显示
  • 先初始化配置,包括我上面说到的 TextKit 中的三个类,还有一些默认的颜色配置,也就是前6行代码。
-(void)setup{
    _textStorage = [NSTextStorage new];
    _layoutManager = [NSLayoutManager new];
    _textContainer = [NSTextContainer new];
    _jp_commonTextColor = [UIColor colorWithRed:162.0/255 green:162.0/255  blue:162.0/255  alpha:162.0/255];
    _jp_textHightLightBackgroundColor = [UIColor colorWithWhite:0.7 alpha:0.2];
    _linkHightColor = _topicHightColor = _agreementHightColor = _userHightColor = [UIColor colorWithRed:64.0/255 green:64.0/255 blue:64.0/255 alpha:1];

    [self prepareTextSystem];
}
复制代码
  • 接下来初始化文本系统。
// 准备文本系统
-(void)prepareTextSystem{
    // 0.准备文本
    [self prepareText];

    // 1.将布局添加到storeage中
    [self.textStorage addLayoutManager:self.layoutManager];

    // 2.将容器添加到布局中
    [self.layoutManager addTextContainer:self.textContainer];

    // 3.让label可以和用户交互
    self.userInteractionEnabled = YES;

    // 4.设置间距为0
    self.textContainer.lineFragmentPadding = 0;
}
复制代码
  • 然后再初始化文字显示容器的尺寸。
// 布局子控件
-(void)layoutSubviews{
    [super layoutSubviews];

    // 设置容器的大小为Label的尺寸
    self.textContainer.size = self.frame.size;
}
复制代码
  • 初始化完成就可以开始处理我们的逻辑了。首先,拿到UILabel中的文字,将文字转换成NSAttributedString,转换完成以后还要主动设置换行,因为如果用户没有设置,我们也不去设置就会导致,整个文字不管多长,都显示在一行。
// 如果用户没有设置lineBreak,则所有内容会绘制到同一行中,因此需要主动设置
-(NSMutableAttributedString *)addLineBreak:(NSAttributedString *)attrString{
    NSMutableAttributedString *attrStringM = [attrString mutableCopy];
    if (attrStringM.length == 0) return attrStringM;

    NSRange range = NSMakeRange(0, 0);
    NSMutableDictionary *attributes = [[attrStringM attributesAtIndex:0 effectiveRange:&range] mutableCopy];
    NSMutableParagraphStyle *paragraphStyle = [attributes[NSParagraphStyleAttributeName] mutableCopy];

    if (paragraphStyle != nil) {
        paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
    }
    else{
        paragraphStyle = [NSMutableParagraphStyle new];
        paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
        attributes[NSParagraphStyleAttributeName] = paragraphStyle;

        [attrStringM setAttributes:attributes range:range];
    }

    return attrStringM;
}
复制代码
  • 添加完换行,我们就可以开始定义显示格式了,包括显示的字号大小和文字颜色。然后再进行正则表达式的关键字的匹配。正则表达式的匹配格式固定,比较简单,这里不细展开了。但是注意,要将匹配到的结果用数组保存起来,保存的时候用到将结构体保存到数组的问题,我在知识点里已经讲过了。
// 准备文本
-(void)prepareText{

    // 1.准备字符串
    NSAttributedString *attrString = nil;
    if (self.attributedText != nil) {
        attrString = self.attributedText;
    }
    else if (self.text != nil){
        attrString = [[NSAttributedString alloc]initWithString:self.text];
    }
    else{
        attrString = [[NSAttributedString alloc]initWithString:@""];
    }

    if (attrString.length == 0) return;

    self.selectedRange = NSMakeRange(0, 0);

    // 2.设置换行模型
    NSMutableAttributedString *attrStringM = [self addLineBreak:attrString];

    // 3.给文本添加显示字号和颜色
    NSDictionary *attr;
    attr = @{
                NSFontAttributeName : self.font,
                NSForegroundColorAttributeName : self.jp_commonTextColor
            };

    [attrStringM setAttributes:attr range:NSMakeRange(0, attrStringM.length)];

    // 4.设置textStorage的内容
    [self.textStorage setAttributedString:attrStringM];

    // 5.匹配URL
    NSArray *linkRanges = [self getLinkRanges];
    self.linkRangesArr = linkRanges;
    for (NSValue *value in linkRanges) {
        NSRange range;
        [value getValue:&range];
        [self.textStorage addAttribute:NSForegroundColorAttributeName value:self.linkHightColor range:range];
    }

    ...

    // 9.更新显示,重新绘制
    [self setNeedsDisplay];
}
复制代码
  • 上面调用 setNeedsDisplay 的时候,系统会触发 drawRect 方法,我们在这里绘制文字并显示是最理想的。这里注意,我们这里自定义了绘制文字了,不需要系统再绘制默认的文字,要实现这个只需要不调用父类的实现,只要不调用 [super drawRect:rect] 就可以达成效果了。
// 重写drawTextInRect方法
-(void)drawRect:(CGRect)rect{
    // 不调用super就不会绘制原有文字
    // [super drawRect:rect];

    // 2.绘制字形
    // 需要绘制的范围
    NSRange range = NSMakeRange(0, self.textStorage.length);
    [self.layoutManager drawGlyphsForGlyphRange:range atPoint:CGPointZero];
}
复制代码

到这里为止,就完成第一步了,也就是自定义显示格式,给关键字着色。

第二,实现关键字的点击响应,并拿到用户点击的关键字段等信息

要实现点击监听,就先要把 LabeluserInteractionEnabled 打开,因为 UILabel 默认是关闭了的。

  • 先实现 touchesBegan 方法,在这个方法里拿到用户点击的点,然后进入到 [self getSelectRange:selectedPoint] 方法里。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 0.记录点击
    self.isSelected = YES;

    // 1.获取用户点击的点
    CGPoint selectedPoint = [[touches anyObject]locationInView:self];

    // 2.获取该点所在的字符串的range
    self.selectedRange = [self getSelectRange:selectedPoint];

    // 3.是否处理了事件
    if (self.selectedRange.length == 0) {
        [super touchesBegan:touches withEvent:event];
    }
}
复制代码
  • 首先需要明确的是,self.layoutManager 保存了所有的文字显示排版的数据,并且每个字符在 self.layoutManager 中都有一个索引编号,这个编号从 0 开始累加。根据传进来的点,利用 self.layoutManager 里保存的文本数据,我们就可以找到这个点对应那个字符的序号。拿到这个序号,我们就可以在我们自己用正则表达式匹配的结果中,查询这个序号是否有对应的关键字。如果有,就把这个关键字的 NSRange 实例返回,如果不存在就返回一个空值。
-(NSRange)getSelectRange:(CGPoint)selectPoint{
    // 0.如果属性字符串为nil,则不需要判断
    if (self.textStorage.length == 0) return NSMakeRange(0, 0);

    // 1.获取选中点所在的下标值(index)
    NSUInteger index = [self.layoutManager glyphIndexForPoint:selectPoint inTextContainer:self.textContainer];

    // 2.判断下标在什么内
    // 2.1.判断是否是一个链接
    for (NSValue *value in self.linkRangesArr) {
        NSRange range;
        [value getValue:&range];
        if (index > range.location && index < range.location + range.length) {
                [self setNeedsDisplay];
                self.tapStyle = HandleStyleLink;
                return range;
        }
    }

    ...

    return NSMakeRange(0, 0);
}
复制代码
  • 接着实现 touchesEnded 方法,当匹配到用户点击的位置有关键字的时候,应该做出适当的反应,比如这里采用的让关键字区域背景颜色高亮一个颜色。要实现这个功能,我们只需要调用 [self setNeedsDisplay] 方法,系统自动会去重新绘制。同时,取出用户点击的关键字段,通过回调把数据传递出去。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    if (self.selectedRange.length == 0) {
        [super touchesEnded:touches withEvent:event];
        return;
    }

    // 0.记录松开
    self.isSelected = NO;

    // 2.重新绘制
    [self setNeedsDisplay];

    // 3.取出内容
    NSString *selectedString = [[self.textStorage string] substringWithRange:self.selectedRange];

    // 3.回调
    switch (self.tapStyle) {
        case HandleStyleAgreement:{
        __weak typeof(self) weakSelf = self;
            if (self.jp_tapOperation) {
                __strong typeof(weakSelf) strongSelf = weakSelf;
                if (!strongSelf) return;
                self.jp_tapOperation(strongSelf, HandleStyleAgreement, selectedString, strongSelf.selectedRange);
            }
        }
        break;

    ...

    default:
        break;
    }
}
复制代码
  • 绘制用户点击时的文字背景颜色。在 drawRect 方法里添加一个开关,当检测到用户点击的时候,就打开开关,绘制点击关键字背景。如果没有点击,或者点击区域不包含关键字,则关闭绘制背景开关。
// 重写drawTextInRect方法
-(void)drawRect:(CGRect)rect{
    // 不调用super就不会绘制原有文字
    // [super drawRect:rect];

    // 1.绘制背景
    if (self.selectedRange.length != 0) {
        // 2.0.确定颜色
        UIColor *selectedColor = self.isSelected ? self.jp_textHightLightBackgroundColor : [UIColor clearColor];

        // 2.1.设置颜色
        [self.textStorage addAttribute:NSBackgroundColorAttributeName value:selectedColor range:self.selectedRange];

        // 2.2.绘制背景
        [self.layoutManager drawBackgroundForGlyphRange:self.selectedRange atPoint:CGPointMake(0, 0)];
    }
}
复制代码

04.尾记

到这里为止,就可以0行代码集成关键字高亮点选响应封装了。源代码在这里GIT地址

05.更新

2016.9.9 为框架加入自定义字符串高亮显示功能,具体使用如下:

// 自定义匹配的文字和颜色
self.textLabel.jp_matchArr = @[
                                @{
                                    @"string" : @"高亮显示",
                                    @"color" : [UIColor colorWithRed:0.55 green:0.86 blue:0.34 alpha:1]
                                }
                            ];
复制代码

效果如下:

NewPan 的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。

NewPan 的文章集合索引

如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy 上给我留言,以及访问我的 Github
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值