CoreText的简单使用(三)

基于前面文章<a href="https://juejin.cn/user/430664724781693">《CoreText的简单使用(二)》</a>的介绍,我们基于CoreText封装了一个简单的文本排版框架,但是它有很大的局限性,因为我们平时开发的时候,如果需要文本排版,很大可能性就是需要一段文本中展示不同样式,可能有多种字体、颜色、大小等。所以基于之前的框架基础上再次进行修改。

定制排版文件格式

我们首先想到对KGCTFrameParser进行修改,因为我们使用UILabel加载富文本的时候,就是使用NSAttributeString来实现文本变色、字体大小变化等。所以我们修改KGCTFrameParser类的方法,使用NSAttributeString作为参数。

修改后KGCTFrameParser类代码如下:

#import <Foundation/Foundation.h>
#import "KGCoreTextData.h"
#import "KGCTFrameParserConfig.h"


NS_ASSUME_NONNULL_BEGIN

@interface KGCTFrameParser : NSObject

+ (KGCoreTextData *)parseContent:(NSAttributedString *)content config:(KGCTFrameParserConfig *)config;

+ (NSDictionary *)attributesWithConfig:(KGCTFrameParserConfig *)config;

@end

NS_ASSUME_NONNULL_END

#import "KGCTFrameParser.h"

@implementation KGCTFrameParser

+ (NSDictionary *)attributesWithConfig:(KGCTFrameParserConfig *)config{
    CGFloat fontSize = config.fontSize;
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    CGFloat lineSpacing = config.lineSpace;
    const CFIndex kNumberOfSettings = 3;
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpacing},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpacing}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
    
    UIColor *textColor = config.textColor;
    
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSForegroundColorAttributeName] = (id)textColor.CGColor;
    dict[NSFontAttributeName] = (__bridge id)fontRef;
    dict[NSParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
    
    CFRelease(theParagraphRef);
    CFRelease(fontRef);
    return dict;
}

+ (KGCoreTextData *)parseContent:(NSAttributedString *)content config:(KGCTFrameParserConfig *)config{
    //创建CTFramesetterRef实例
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    //获得要绘制的区域的高度
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
    CGFloat textHeight = coreTextSize.height;
    
    //生成CTFrameRef实例
    CTFrameRef frame = [self createFrameWithFrameSetter:frameSetter config:config height:textHeight];
    
    //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回实例
    KGCoreTextData *data = [[KGCoreTextData alloc] init];
    data.ctFrame = frame;
    data.height = textHeight;
    CFRelease(frame);
    CFRelease(frameSetter);
    return data;
}

+ (CTFrameRef)createFrameWithFrameSetter:(CTFramesetterRef)frameSetter config:(KGCTFrameParserConfig *)config height:(CGFloat)height{
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
    CFRelease(path);
    return frame;
}

@end

然后在KGCoreTextCtrl中进行配置,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    KGCTFrameParserConfig *config = [[KGCTFrameParserConfig alloc] init];
    config.textColor = [UIColor blackColor];
    config.width = self.ctView.width;
    
    NSString *str = @"这是一个测试代码,主要是为了展示富文本,前面一句话显示红色,第二句话显示绿色,三句话字体放大";
    NSDictionary *attr = [KGCTFrameParser attributesWithConfig:config];
    
    NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:str attributes:attr];

    [attributeString addAttributes:@{NSForegroundColorAttributeName:[UIColor redColor]} range:NSMakeRange(0, 9)];
    [attributeString addAttributes:@{NSForegroundColorAttributeName:[UIColor greenColor]} range:NSMakeRange(10, 10)];
    [attributeString addAttributes:@{NSForegroundColorAttributeName:[UIColor blueColor],NSFontAttributeName:KGFont(20.0)} range:NSMakeRange(21, 10)];
    
    KGCoreTextData *data = [KGCTFrameParser parseContent:attributeString config:config];
    
    self.ctView.data = data;
    self.ctView.height = data.height;
    self.ctView.backgroundColor = [UIColor yellowColor];
}

- (KGDisPlayView *)ctView{
    if (!_ctView) {
        self.ctView = [[KGDisPlayView alloc] initWithFrame:CGRectMake(50, 100, self.view.frame.size.width - 100, 300)];
        [self.view addSubview:self.ctView];
    }
    return _ctView;
}

最后运行项目,查看效果,如下图所示:

20200810003.png

但是在实际开发的时候,不会只是简简单单显示这种,而且前后端数据交互啥的,我们拿到是这种的数据的话,处理起来很麻烦,所以一般都是和后台进行约定,规定一种格式,方便前端进行使用,最常见的就是JSON格式直接返回排版需要的配置,例如:

[
    {
        "color":"red",
        "size":"12",
        "content":"但是在实际开发的时候,不会只是简简单单显示这种,而且前后端数据交互啥的",
        "type":"txt"
    },
    {
        "color":"green",
        "size":"16",
        "content":"我们拿到是这种的数据的话,处理起来很麻烦,所以一般都是和后台进行约定",
        "type":"txt"
    },
    {
        "color":"blue",
        "size":"20",
        "content":"规定一种格式,方便前端进行使用,最常见的就是JSON格式直接返回排版需要的配置",
        "type":"txt"
    }
]

我以前做一个类似微博广场功能块的图文混排时候,后台返回的就是这种数据类型,当我们得到这样的JSON数据后,我们可以使用优秀的三方库进行数据解析JSONKit,也可以通过系统提供的NSJSONSerialization进行解析,得到NSDictionary对象的数据组后,通过以下一系类方法进行转换得到我们绘制需要的数据。

首先我们需要将接受到的JSON数据进行解析,然后读取JSON数据返回的配置以及内容,得到一个富文本,然后根据富文本去生成绘制模型,具体代码如下:

#pragma mark --根据JSON数据,创建富文本

+ (NSDictionary *)attributesWithConfig:(KGCTFrameParserConfig *)config{
    CGFloat fontSize = config.fontSize;
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    CGFloat lineSpacing = config.lineSpace;
    const CFIndex kNumberOfSettings = 3;
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpacing},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpacing}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
    
    UIColor *textColor = config.textColor;
    
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSForegroundColorAttributeName] = (id)textColor.CGColor;
    dict[NSFontAttributeName] = (__bridge id)fontRef;
    dict[NSParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
    
    CFRelease(theParagraphRef);
    CFRelease(fontRef);

    return dict;
}

+ (CTFrameRef)createFrameWithFrameSetter:(CTFramesetterRef)frameSetter config:(KGCTFrameParserConfig *)config height:(CGFloat)height{
    //创建一个可变路径,图形上下文中要绘制的图形或线条的数学描述
    CGMutablePathRef path = CGPathCreateMutable();
    //追加矩形到可变路径
    CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
    //对核心文本框架对象的引用
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
    CFRelease(path);
    return frame;
}

+ (UIColor *)colorFromDictionary:(NSDictionary *)dict{
    if ([dict[@"color"] isEqualToString:@"red"]) {
        return [UIColor redColor];
    } else if ([dict[@"color"] isEqualToString:@"blue"]) {
        return [UIColor blueColor];
    } else if ([dict[@"color"] isEqualToString:@"green"]) {
        return [UIColor greenColor];
    }else{
        return nil;
    }
}

+ (NSAttributedString *)attributedingWithDictionary:(NSDictionary *)dic config:(KGCTFrameParserConfig *)config{
    //创建一个可变字典,保存默认富文本信息配置选项
    NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]];
    //获取数据中颜色值
    UIColor *color = [self colorFromDictionary:dic];
    if (color) {
        //如果数据中给出颜色值,替换默认设置的色值
        attributes[NSForegroundColorAttributeName] = color;
    }
    CGFloat fontSize = [dic[@"size"] floatValue];
    if (fontSize > 0) {
        //获取数据返回的字体大小,如果存在,创建一个CTFontRef实例,替换默认设置项中的字体设置
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        attributes[NSFontAttributeName] = (__bridge id)fontRef;
        //释放CTFontRef实例
        CFRelease(fontRef);
    }
    NSString *content = dic[@"content"];
    //根据富文本培训选项以及给定字符串创建并返回一个富文本
    return [[NSAttributedString alloc] initWithString:content attributes:attributes];
}

+ (NSAttributedString *)loadJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config{
    //JSON字符串转NSData
    NSData *data = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
    //创建一个可变富文本,保存JSON数据
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    //这里做下容错处理,如果传入的json数据是非空并且有内容,进行数据转换
    if (data) {
        //因为JSON数据本身最外层就是一个数组,所以这块需要用数组去接受JSON解析后得到的值
        NSArray *arr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        //这里再做下容错处理,如果是NSArray类,那就说明数据没有问题,可以进行下一步操作
        if ([arr isKindOfClass:[NSArray class]]) {
            //直接使用for in遍历
            for (NSDictionary *dict in arr) {
                NSString *type = dict[@"type"];
                //判断type类型是否是txt,因为现在只是对txt类型进行的处理,所以这块需要进行过滤
                if ([type isEqualToString:@"txt"]) {
                    //获取到单条数据富文本信息
                    NSAttributedString *as = [self attributedingWithDictionary:dict config:config];
                    //将单条数据富文本信息拼接到总数据中
                    [result appendAttributedString:as];
                }
            }
        }
    }
    return result;
}

+ (KGCoreTextData *)parseJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config{
    //通过JSON数据,得到富文本
    NSAttributedString *content = [self loadJSONContent:jsonContent config:config];
    //创建CTFramesetterRef实例
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    //设置绘制边界大小
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    //获得实际绘制所需要的大小
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
    //因为宽度已经国定,所以在这获取实际绘制需要的高度
    CGFloat textHeight = coreTextSize.height;
    //生成CTFrameRef实例
    CTFrameRef frame = [self createFrameWithFrameSetter:frameSetter config:config height:textHeight];
    //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回实例
    KGCoreTextData *data = [[KGCoreTextData alloc] init];
    //设置实际绘制需要的实例对象
    data.ctFrame = frame;
    //设置实际绘制需要的高度
    data.height = textHeight;
    //因为底层库不受ARC约束,所以需要手动调用CFRelease来进行释放
    //释放生成的CTFrameRef实例
    CFRelease(frame);
    //释放CTFramesetterRef实例
    CFRelease(frameSetter);
    //返回需要的绘制数据模型
    return data;
}

方法里面的注释写的很清晰,所以不做多的介绍。我们在KGDisPlayView内部的设置还是不变,只需要在ViewController中对KGCTFrameParser进行配置,代码如下:

#import "KGCoreTextCtrl.h"
#import "KGDisPlayView.h"
#import "KGCTFrameParserConfig.h"
#import "KGCoreTextData.h"
#import "KGCTFrameParser.h"
#import <CoreText/CoreText.h>

@interface KGCoreTextCtrl ()

@property (nonatomic, strong) KGDisPlayView *ctView;

@end

@implementation KGCoreTextCtrl

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    KGCTFrameParserConfig *config = [[KGCTFrameParserConfig alloc] init];
    config.textColor = [UIColor blackColor];
    config.width = self.ctView.width;
    
    NSArray *arr = @[
    @{
        @"type":@"txt",
        @"content":@"但是在实际开发的时候,不会只是简简单单显示这种,而且前后端数据交互啥的,",
        @"size":@"12",
        @"color":@"red"
    },
    @{
        @"type":@"txt",
        @"content":@"我们拿到是这种的数据的话,处理起来很麻烦,所以一般都是和后台进行约定,",
        @"size":@"16",
        @"color":@"green"
    },
    @{
        @"type":@"txt",
        @"content":@"规定一种格式,方便前端进行使用,最常见的就是JSON格式直接返回排版需要的配置",
        @"size":@"20",
        @"color":@"blue"
    }
    ];
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:arr options:NSJSONWritingFragmentsAllowed error:nil];
    KGCoreTextData *data = [KGCTFrameParser parseJSONContent:[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] config:config];
    
    self.ctView.data = data;
    self.ctView.height = data.height;
    self.ctView.backgroundColor = [UIColor yellowColor];
}

- (KGDisPlayView *)ctView{
    if (!_ctView) {
        self.ctView = [[KGDisPlayView alloc] initWithFrame:CGRectMake(50, 100, self.view.frame.size.width - 100, 300)];
        [self.view addSubview:self.ctView];
    }
    return _ctView;
}


@end

最后得到的效果如下图所示:

20200811001.png

到此我们的框架可以支持富文本格式的文本渲染了,下一篇,开始探索图文混排。

系列文章:

<a href="https://juejin.cn/post/6970879379425460255">《CoreText的简单使用(一)》</a>

<a href="https://juejin.cn/user/430664724781693">《CoreText的简单使用(二)》</a>

<a href="https://juejin.cn/post/6970880935327694879/">《CoreText的简单使用(三)》</a>

<a href="https://juejin.cn/post/6970881236936081439/">《CoreText的简单使用(四)》</a>

<a href="https://juejin.cn/post/6970881873304092686/">《CoreText的简单使用(五)》</a>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358

推荐阅读更多精彩内容

  • CoreText的简单使用(一) 介绍: 读唐巧的《iOS开发进阶》,关于CoreText的笔记。借用介绍:“使用...
    KG丿夏沫阅读 1,102评论 0 0
  • 基于上一篇 《CoreText的简单使用(一)》 的介绍,我们再次探索下,毕竟对于我们开发来说,功能代码都是需要能...
    KG丿夏沫阅读 245评论 0 0
  • 目录 1.解释self = [super init]方法 容错处理,当父类初始化失败,会返回一个nil,表示初始化...
    勇敢的_心_阅读 1,395评论 0 4
  • CoreText是iOS/OSX中文本显示的一个底层框架,它是用C语言写成的,有快速简单的优势。iOS中的Text...
    小猫仔阅读 4,979评论 2 9
  • 闲来无事想着自己搞个富文本的工具库,不至于每次遇见这些东西就用别人的第三方。自己研究研究也有助于自己对这方面的理解...
    傲骨天成科技阅读 2,437评论 4 9