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阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容