TextKit框架詳細解析 (一) —— 基本概覽和應用場景(一)

版本記錄

版本號 時間
V1.0 2018.08.29

前言

TextKit框架是對Core Text的封裝,用簡潔的調用方式實現了大部分Core Text的功能。 TextKit是一個偏上層的開發框架,在iOS7以上可用,使用它可以方便靈活處理復雜的文本布局,滿足開發中對文本布局的各種復雜需求。TextKit實際上是基于CoreText的一個上層框架,其是面向對象的。接下來幾篇我們就一起看一下這個框架。

簡介

注意:TextKit 框架是對Core Text的封裝,是iOS7以上才可以使用的框架,是文字排版和渲染引擎。

1. 幾種渲染控件

在iOS開發中,處理文本的視圖控件主要有4中,UILabelUITextFieldUITextViewUIWebView。其中UILabel與UITextField相對簡單,UITextView是功能完備的文本布局展示類,通過它可以進行復雜的富文本布局,UIWebView主要用來加載網頁或者pdf文件,其可以進行HTMLCSSJS等文件的解析。

2. 文字框架層級關系

在iOS 7之前的文字渲染框架層級主要如下所示:

iOS 7之前

在iOS 7之后,文字渲染框架的層級主要如下所示:

iOS 7之后

3. 包含的主要的類和關系

下面我們就看一下該框架包含的類及其關系,如下所示:

  • NSTextStorage:是NSMutableAttributedString的子類,負責存儲需要處理的文本及其屬性。
    • 1)NSTextStorage集成父類的所有的屬性,但NSTextStorage包含了一個方法,可以將所有對其內容進行的修改以通知的方式發送出來。
    • 2)使用一個自定義的NSTextStorage就可以讓文本在稍后動態地添加字體或者顏色高亮等文本屬性修飾。
  • NSLayoutManager:負責將NSTextStorage中的文本數據渲染到顯示區域上,負責字符的編碼和布局。他管理著每一個字符的顯示。
    • 1)與NSTextStorage的關系:它監聽著NSTextStorage發出的關于string屬性改變的通知,一旦接受到通知就會觸發重新布局。
    • 2)從NSTextStorage中獲取string(內容)將其轉化為字形(與當前設置的字體等內容相關)。
    • 3)一旦字形完全生成完畢,NSLayoutManager(管理者)會向NSTextContainer查詢文本可用的繪制區域。
    • 4)NSTextContainer會將文本的當前狀態改為無效,然后交給textView去顯示。
  • NSTextContainer:描述了一個顯示區域,默認是矩形,其子類可以定義任意的形狀。它不僅定義了可填充的區域,而且內部還定義了一個不可填充區域(Bezier Path 數組)。

數據流程如下圖所示:

數據流程圖

這個類的關系如下所示:

通常情況下,一個NSTextStorage 對應 一個NSLayoutManager 對應 一個 NSTextContainer

當文字顯示為多列、多頁時,一個NSLayoutManager對應多個 NSTextContainer

當采用不同的排版方式時,一個NSTextStorage對應多個NSLayoutManager

通常由NSLayoutManagerNSTextStorage中讀取出文本數據,然后根據一定的排版方式,將文本排版到NSTextContainer中,再由NSTextContainer結合UITextView將最終效果顯示出來。

4. 進行文本布局渲染的流程

下面我們就主要看一下用TextKit進行文本布局流程。

使用TextKit進行文本的布局展示十分繁瑣,首先需要將顯示內容定義為一個NSTextStorage對象,之后為其添加一個布局管理器對象NSLayoutManager,在NSLayoutManager中,需要進行NSTextContainer的定義,定義多了NSTextContainer對象則會將文本進行分頁。最后,將要展示的NSTextContainer綁定到具體的UITextView視圖上。

5. 幾種常見功能

下面看一下TextKit可以實現的功能:

  • 字距調整(Kerning)
  • 連寫
  • 圖像附件:可以向TextView里添加圖像了
  • 斷字:設置hyphenationFactor就可以啟用斷字
  • 可定制性
  • 更多的富文本屬性:設置不同的下劃線,雙線,粗線,虛線,點線或者他們的組合。
  • 序列化
  • 文本樣式:全局預定義文本類型
  • 文本效果:使用這個效果的文本看起來就像蓋紙上面一樣。

后面會進行詳細的示例和說明。


幾中應用場景和簡單示例

下面我們就簡單的看一下幾種應用場景和簡單示例。這里由于篇幅的限制可能一篇不能完成,那么會在多篇中逐步完善。

1. UILabel不同文字的點擊

大家都知道UILabel是可以進行交互的,那需要打開UserInterfaceEnabled,因為默認的交互是關著的。但是即使打開的話我們添加手勢,如果不判斷點擊區域的話,那么接收手勢響應的就是整個UILabel控件,但是利用TextKit框架就可以實現UILabel不同區域文字的點擊。

下面首先還是先看代碼,主要是兩個類 —— JJLabelJJLabelVC

  • JJLabel
  • JJLabelVC

下面就看一下源碼。

1. JJLabelVC.h
import <UIKit/UIKit.h>

@interface JJLabelVC : UIViewController

@end
2. JJLabelVC.m
#import "JJLabelVC.h"
#import "JJLabel.h"

@interface JJLabelVC ()

@property (nonatomic, strong) JJLabel *label;
@property (nonatomic, strong) UILabel *infoLabel;

@end

@implementation JJLabelVC

#pragma mark -  Override Base Function

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.view.frame = [UIScreen mainScreen].bounds;
    self.view.backgroundColor = [UIColor lightGrayColor];
    
    [self initUI];
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    
    self.label.frame = CGRectMake(50, 100, 300, 40);
    self.infoLabel.frame = CGRectMake(50, 400, 300, 40);
}

#pragma mark -  Object Private Function

- (void)initUI
{
    //詳情label
    self.infoLabel = [[UILabel alloc] init];
    self.infoLabel.backgroundColor = [UIColor magentaColor];
    self.infoLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:self.infoLabel];
    
    //展示label
    self.label = [[JJLabel alloc] init];
    self.label.textAlignment = NSTextAlignmentCenter;
    self.label.backgroundColor = [UIColor greenColor];
    
    NSMutableAttributedString *attributedString1 = [[NSMutableAttributedString alloc] initWithString:@"讓我們"];
    [attributedString1 addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, attributedString1.length)];
    
    [self.label appendString:attributedString1 block:^(NSAttributedString *attributeString) {
        self.infoLabel.text = @"讓我們";
        NSLog(@"%@", attributedString1);
    }];
    
    NSMutableAttributedString *attributedString2 = [[NSMutableAttributedString alloc] initWithString:@"蕩起"];
    [attributedString2 addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, attributedString2.length)];
    [self.label appendString:attributedString2 block:^(NSAttributedString *attributeString) {
        self.infoLabel.text = @"蕩起";
        NSLog(@"%@", attributedString2);
    }];
    
    NSMutableAttributedString *attributedString3 = [[NSMutableAttributedString alloc] initWithString:@"雙槳"];
    [attributedString3 addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(0, attributedString3.length)];
    [self.label appendString:attributedString3 block:^(NSAttributedString *attributeString) {
        self.infoLabel.text = @"雙槳";
        NSLog(@"%@", attributedString3);
    }];
    [self.view addSubview:self.label];
}

@end
3. JJLabel.h
#import <UIKit/UIKit.h>

typedef void(^textKitStringBlock)(NSAttributedString *attributeString);

@interface JJLabel : UILabel

//字符串更新并添加回調
- (void)appendString:(NSAttributedString *)attributeString block:(textKitStringBlock)block;

@end
4. JJLabel.m
#import "JJLabel.h"

@interface JJLabel()

@property (nonatomic, strong) NSMutableArray <NSAttributedString *> *subAttributedStringsArrM;
@property (nonatomic, strong) NSMutableArray <NSValue *> *subAttributedStringRangesArrM;
@property (nonatomic, strong) NSMutableArray <textKitStringBlock> *stringOptionsArrM;
@property (nonatomic, strong) NSTextStorage *textStorage;
@property (nonatomic, strong) NSLayoutManager *layoutManager;
@property (nonatomic, strong) NSTextContainer *textContainer;

@end

@implementation JJLabel

#pragma mark -  Override Base Function

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.userInteractionEnabled = YES;
        self.subAttributedStringsArrM = [NSMutableArray array];
        self.subAttributedStringRangesArrM = [NSMutableArray array];
        self.stringOptionsArrM = [NSMutableArray array];
        [self setupSystemTextKitConfiguration];
    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    
    self.textContainer.size = self.bounds.size;
}

- (void)drawTextInRect:(CGRect)rect
{
    NSRange range = NSMakeRange(0, self.textStorage.length);
    [self.layoutManager drawBackgroundForGlyphRange:range atPoint:CGPointMake(0.0, 0.0)];
    [self.layoutManager drawGlyphsForGlyphRange:range atPoint:CGPointMake(0.0, 0.0)];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    //根據點來獲取該位置glyph的index
    NSInteger glythIndex = [self.layoutManager glyphIndexForPoint:point inTextContainer:self.textContainer];
    //獲取改glyph對應的rect
    CGRect glythRect = [self.layoutManager boundingRectForGlyphRange:NSMakeRange(glythIndex, 1) inTextContainer:self.textContainer];
    //最終判斷該字形的顯示范圍是否包括點擊的location
    if (CGRectContainsPoint(glythRect, point)) {
        NSInteger characterIndex = [self.layoutManager characterIndexForGlyphAtIndex:glythIndex];
        [self.subAttributedStringRangesArrM enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSRange range = obj.rangeValue;
            if (NSLocationInRange(characterIndex, range)) {
                textKitStringBlock block = self.stringOptionsArrM[idx];
                block(self.subAttributedStringsArrM[idx]);
            }
        }];
    }
}

#pragma mark -  Object Private Function

- (void)setupSystemTextKitConfiguration
{
    self.textStorage = [[NSTextStorage alloc] init];
    self.layoutManager = [[NSLayoutManager alloc] init];
    self.textContainer = [[NSTextContainer alloc] init];
    
    [self.textStorage addLayoutManager:self.layoutManager];
    [self.layoutManager addTextContainer:self.textContainer];
}

#pragma mark - Object Public Function

//字符串更新并添加回調
- (void)appendString:(NSAttributedString *)attributeString block:(textKitStringBlock)block
{
    [self.subAttributedStringsArrM addObject:attributeString];
    
    NSRange range = NSMakeRange(self.textStorage.length, attributeString.length);
    [self.subAttributedStringRangesArrM addObject:[NSValue valueWithRange:range]];
    
    [self.stringOptionsArrM addObject:block];
    [self.textStorage appendAttributedString:attributeString];
}

#pragma mark -  Getter && Setter

- (void)setText:(NSString *)text
{
    [super setText:text];
    
    [self setupSystemTextKitConfiguration];
}

- (void)setAttributedText:(NSAttributedString *)attributedText
{
    [super setAttributedText:attributedText];
    
    [self setupSystemTextKitConfiguration];
}

@end

下面看一下實現效果

下面看一下控制臺輸出:

2018-08-28 14:47:37.224444+0800 JJTextKit[16504:511572] 讓我們{
    NSColor = "UIExtendedSRGBColorSpace 0 0 1 1";
}
2018-08-28 14:47:38.712045+0800 JJTextKit[16504:511572] 蕩起{
    NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
}
2018-08-28 14:47:39.727875+0800 JJTextKit[16504:511572] 雙槳{
    NSColor = "UIExtendedSRGBColorSpace 0 0 1 1";
}
2018-08-28 14:47:40.959816+0800 JJTextKit[16504:511572] 蕩起{
    NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
}
2018-08-28 14:47:41.703850+0800 JJTextKit[16504:511572] 讓我們{
    NSColor = "UIExtendedSRGBColorSpace 0 0 1 1";
}

2. 部分文字的高亮等特殊顯示

對于這個問題,如果你看過我寫的前面幾篇文章,那么可以在那里找到答案,這里只給出鏈接不做具體說明了。

實用小技巧(三十四)—— 設置一個UILabel控件不同行文字字體樣式以及行間距等個性化設置(一)

接下來我們要做的是用另外一種方法進行實現。

還是先看一下代碼:

1. JJHighlightVC.h
#import <UIKit/UIKit.h>

@interface JJHighlightVC : UIViewController

@end
2. JJHighlightVC.m
#import "JJHighlightVC.h"
#import "JJHighlightStorage.h"

@interface JJHighlightVC ()

@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) JJHighlightStorage *textStorage;
@property (nonatomic, strong) NSTextContainer *textContainer;
@property (nonatomic, strong) NSLayoutManager *layoutManager;

@end

@implementation JJHighlightVC

#pragma mark -  Override Base Function

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.textContainer = [[NSTextContainer alloc] init];
    self.layoutManager = [[NSLayoutManager alloc] init];
    self.textStorage = [[JJHighlightStorage alloc] init];
    [self.textStorage addLayoutManager:self.layoutManager];
    [self.layoutManager addTextContainer:self.textContainer];
    
    self.textView = [[UITextView alloc] initWithFrame:CGRectMake(10.0, 250.0, self.view.bounds.size.width - 20.0, 200) textContainer:self.textContainer];
    self.textView.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview:self.textView];
    
    [self.textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:@"波浪線引起來的字符都會被~變為藍色~,對,是這樣的"];
}

@end
3. JJHighlightStorage.h
#import <UIKit/UIKit.h>

@interface JJHighlightStorage : NSTextStorage

@end
4. JJHighlightStorage.m
#import "JJHighlightStorage.h"

@interface JJHighlightStorage()

@property (nonatomic, strong) NSMutableAttributedString *mutableAttributedString;
@property (nonatomic, strong) NSRegularExpression *expression;

@end

@implementation JJHighlightStorage

#pragma mark -  Override Base Function

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.mutableAttributedString = [[NSMutableAttributedString alloc] init];
        self.expression = [NSRegularExpression regularExpressionWithPattern:@"(\\~\\w+(\\s*\\w+)*\\s*\\~)" options:0 error:NULL];
    }
    return self;
}

- (NSString *)string
{
    return self.mutableAttributedString.string;
}

- (NSDictionary<NSString *,id> *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
    return [self.mutableAttributedString attributesAtIndex:location effectiveRange:range];
}

- (void)setAttributes:(NSDictionary<NSString *,id> *)attrs range:(NSRange)range
{
    [self beginEditing];
    [self.mutableAttributedString setAttributes:attrs range:range];
    [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
    [self endEditing];
}

// Sends out -textStorage:willProcessEditing, fixes the attributes, sends out -textStorage:didProcessEditing, and notifies the layout managers of change with the -processEditingForTextStorage:edited:range:changeInLength:invalidatedRange: method.  Invoked from -edited:range:changeInLength: or -endEditing.
- (void)processEditing
{
    [super processEditing];
    
    //去除當前段落的顏色屬性
    NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
    [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];
    //根據正則匹配,添加新屬性
    [self.expression enumerateMatchesInString:self.string options:NSMatchingReportProgress range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
        [self addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:result.range];
    }];
}

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
    [self beginEditing];
    [self.mutableAttributedString replaceCharactersInRange:range withString:str];
    [self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length];
    [self endEditing];
}

@end

下面看一下效果


參考文章

1. 最詳細TextKit分析
2. TextKit框架
3. TextKit功能和結構
4. TextKit介紹(轉載3篇文章)
5. TextKit詳解
6. 學習TextKit框架(上)
7. iOS Text Part1:TextKit
8. TextKit 探究

后記

本篇主要講述了TextKit框架基本概覽和兩個常用的使用場景,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容