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

你們都用過微博,微博有用戶「@盼盼」,話題「#怎麼追漂亮女孩?#」,還有網絡連接「http: //...」,還有各類協議「《FBI WARNING...》」,實際項目中要求給這些特殊的字符着色,就像 demo 這樣。這篇文章將帶你封裝這個功能,而且在實際開發中 0 行代碼集成。git

JPLabel.gif

01. 怎麼集成到你的項目?

若是你趕項目沒時間看源碼,只須要看一下下面的使用就能夠了。github

  • 下載個人源碼(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 文檔中找出來的圖:正則表達式

基本的Text Kit對象.png

這裏我簡單的描述一下每一個類的做用,具體你若是感興趣,能夠本身去看文檔,或者看這篇文檔的中文翻譯文章swift

  • 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地址app

05.更新

2016.9.9 爲框架加入自定義字符串高亮顯示功能,具體使用以下:框架

// 自定義匹配的文字和顏色
self.textLabel.jp_matchArr = @[
                                @{
                                    @"string" : @"高亮顯示",
                                    @"color" : [UIColor colorWithRed:0.55 green:0.86 blue:0.34 alpha:1]
                                }
                            ];
複製代碼

效果以下:工具

JPLabel.png

NewPan 的文章集合

下面這個連接是我全部文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有源碼。

NewPan 的文章集合索引

若是你有問題,除了在文章最後留言,還能夠在微博 @盼盼_HKbuy 上給我留言,以及訪問個人 Github

相關文章
相關標籤/搜索