CoreText实现添加标签

CoreText实现添加标签

CoreText是基于C语言的API设计,主要通过UIView的drawRect:来实现自定义的文案排版,但是在OS X系统上放回的确实NSRect的一个对象,这个时候你可以使用的NSRectToCGRect这个函数来将结果做一下转化,以适应更多的平台。这里有个需要注意的点:图形环境是根据UIGraphicsGetCurrentContext此函数的结果,但是在这个结果的参考系是左下角,ios的参考系确实在左上角,所以,如果你是在ios上做CoreText对文案的排版,你需要转换一下参考系。由于CoreText是基于C的语言所以在速度上有所提升,在语言上也比较简单。

CoreText渲染引擎使用的场景经常是attributeString/graphicsPath之类,这些对象在被渲染的时候都是包含自己的Properties(or “attributes”),这样引擎就可以知道这些文字或是图形将要被渲染成什么样式。

下面来看一组图片,CoreText是在运行时构建的多层次的文本对象,如图,在这个层次的最顶端是一个frameSetter对象,当有一个文本或是一个图形输入的时候,frameSetter就会生成一个或是多个文本(CTFrameRef),每个CTFrame对象代表一个段落

为了生成可是的文本,frameSetter会调用一个叫CTTypesetterRef的对象,然后将FrameSetter将相应的段落信息,行信息,等等属性,转化成AttributeString放进排版的文本中去

Font Obejcts

字体对象用来制定一个字符的字体size等表现,你可以用多线程的思想来创建多个fontObejct,以便于使用。同时当我们制定对应文本的时候,在系统中会有这样一个字体表,或从中匹配一个默认值做显示。同时也是可以自定义这个字体表的存取的。在创建FontObejct的时候,你可以使用已有的fontDescriptors,也可以自定义一个出来,FontDescriptor对象是一个相当于字典的对象。可以很方便的充中拿到你想要的信息。同时你可以将你自定义的字体对象放到一个Collections中,这个Collection可以为你提供遍历和存储的功能,你可以很方便从中去到你已经存储的字体集,从而统一实现app的文字展示

在这之后的Apple 原文还是不翻译了,最终起效果的就是上面的图片所示的内容。主要的就是一个CTFrameSetter这样的一个排版设置,下面就让我们一起来看一下这个函数中都包含了那些信息。下面的只是简介都会使用简短代码的方式,完整的代码整理后放到了这里

分析一下CoreText/CTFramesetter.h文件

// Typedef 定义了一个CTFrameSetterRef的对象以便于使用
typedef const struct CF_BRIDGED_TYPE(id) __CTFramesetter * CTFramesetterRef;

// 得到类型ID
CFTypeID CTFramesetterGetTypeID( void ) CT_AVAILABLE(10_5, 3_2);

// 下面是创建FrameSetter

/*!
    @摘要    通过attributeString创建属性不变的FrameSetter对象

    @描述 结果FrameSetter对象可以用来创建和填充文本框通过调用CTFrameSetterCreateFrame。

    @参数        string
                需要绘制的文字

    @结果    创建成功返回一个CTFramesetterRef引用,否则返回NULL
*/

CTFramesetterRef CTFramesetterCreateWithAttributedString(
    CFAttributedStringRef string ) CT_AVAILABLE(10_5, 3_2);


/* --------------------------------------------------------------------------- */
/* Frame Creation */
/* --------------------------------------------------------------------------- */

/*!
    @摘要    通过如上所述的FrameSetter创建一个不可变的Frame对象.

    @描述  这个调用将创建一个边框,这个边框的定义来自于参数Path.排版也将这个这边框中执行,
           直到排版完成,或这个边框已经被排版完全为止

    @param        framesetter
                就像上面创建的FrameSetter对象

    @param        stringRange
                这个StringRange是一个结合attributeString使用的一个对象,当你指定了{location,length}之后,
                排版就会将这段文字绘制在当前的Context中。当你制定了length=0,则在location之后的文字都会被会在到context中

    @param        path
                给出这个排版的区域

    @param        frameAttributes
                和attributeString中设置属性一样,可以单独指定

    @result     返回一个CTFrame对象
*/

CTFrameRef CTFramesetterCreateFrame(
    CTFramesetterRef framesetter,
    CFRange stringRange,
    CGPathRef path,
    CFDictionaryRef __nullable frameAttributes ) CT_AVAILABLE(10_5, 3_2);


/*!
    @摘要    从边框中获取typesetter

    @discussion Each framesetter uses a typesetter internally to perform
                line breaking and other contextual analysis based on the
                characters in a string; this function returns the typesetter
                being used by a particular framesetter if the caller would
                like to perform other operations on that typesetter.

    @param        framesetter
                The framesetter from which a typesetter is being requested.

    @result        This function will return a reference to a CTTypesetter
                object, which should not be released by the caller.
*/

CTTypesetterRef CTFramesetterGetTypesetter(
    CTFramesetterRef framesetter ) CT_AVAILABLE(10_5, 3_2);


// 边框尺寸

/*!
    @摘要    确定字符串范围所需的边框大小大小。    
    @描述    这个函数可用来确定一个字符串在文本中显示,需要多大的空间。可选的参考类型有两种一个是约束,一个是给定的尺寸

    @param        framesetter
                衡量边框的大小

    @param        stringRange
                这个StringRange是一个结合attributeString使用的一个对象,当你指定了{location,length}之后,
                排版就会将这段文字绘制在当前的Context中。当你制定了length=0,则在location之后的文字都会被会在到context

    @param        frameAttributes
                指定的文字展示样式

    @param        constraints
                给定一个尺寸用于展示,也可以指定width,和height都是max

    @param        fitRange
                实际适用于约束大小的字符串的范围。

    @result        The actual dimensions for the given string range and constraints.
*/

CGSize CTFramesetterSuggestFrameSizeWithConstraints
(
    CTFramesetterRef framesetter,
    CFRange stringRange,
    CFDictionaryRef __nullable frameAttributes,
    CGSize constraints,
    CFRange * __nullable fitRange ) CT_AVAILABLE(10_5, 3_2);

上面的就是CTFrameSetter.h函数的全部API,其实也不是很多。接下来了解一下CTFrame.h头文件

// 获取CTFrame类型
CFTypeID CTFrameGetTypeID( void ) CT_AVAILABLE(10_5, 3_2);

// 这个枚举类型主要给出的就是文字排版的方向
typedef CF_ENUM(uint32_t, CTFrameProgression) {
    kCTFrameProgressionTopToBottom  = 0, // 从上到下的水平布局方式
    kCTFrameProgressionRightToLeft  = 1, // 从由到左的垂直布局方式
    kCTFrameProgressionLeftToRight  = 2  // 从左到右的垂直布局方式
};

// 排版的方向,值必须是一个CFNumberRef类型的值,默认是kCTFrameProgressionTopToBottom
extern const CFStringRef kCTFrameProgressionAttributeName CT_AVAILABLE(10_5, 3_2);

// 下面是两种填充规则
typedef CF_ENUM(uint32_t, CTFramePathFillRule) {
    kCTFramePathFillEvenOdd         = 0,
    kCTFramePathFillWindingNumber   = 1
};

// 对应上面的填充规则的key    
extern const CFStringRef kCTFramePathFillRuleAttributeName CT_AVAILABLE(10_7, 4_2);

// 边框宽度的key,value是一个CFNumberRef的对象
extern const CFStringRef kCTFramePathWidthAttributeName CT_AVAILABLE(10_7, 4_2);

// 这个key是一个很实用的key,例如在排版的时候你想忽略掉某个CGPathRef,将这个CGPathRef作为Value添加在这个属性中就可以了忽略了。    
extern const CFStringRef kCTFrameClippingPathsAttributeName CT_AVAILABLE(10_7, 4_3);

// 当只有一个CGPathRef需要忽略的时候可以实用这个key
extern const CFStringRef kCTFramePathClippingPathAttributeName CT_AVAILABLE(10_7, 4_3);

// 从Frame中拿到已经填充的字体的range,请求失败的话,返回一个空的range
CFRange CTFrameGetStringRange(
CTFrameRef frame ) CT_AVAILABLE(10_5, 3_2);

// 只显示已经排版的range,这个情况会出现在,当rect很小,但是文字很多的时候,这个时候就可以实用这个函数,知道那些字是被排版上了的
CFRange CTFrameGetVisibleStringRange(
CTFrameRef frame ) CT_AVAILABLE(10_5, 3_2);

// 从CTFrameRef中返回一个CGPathRef
CGPathRef CTFrameGetPath(
CTFrameRef frame ) CT_AVAILABLE(10_5, 3_2);

// 返回这个CTFrameRef中的所有属性设置
CFDictionaryRef __nullable CTFrameGetFrameAttributes(
CTFrameRef frame ) CT_AVAILABLE(10_5, 3_2);

// 返回一个排版的行的数组
CFArrayRef CTFrameGetLines(
CTFrameRef frame ) CT_AVAILABLE(10_5, 3_2);

// 返回行的原点坐标数组,range是文字的range。frame是
void CTFrameGetLineOrigins(
CTFrameRef frame,
CFRange range,
CGPoint origins[] ) CT_AVAILABLE(10_5, 3_2);

// frame绘制
void CTFrameDraw(
CTFrameRef frame,
CGContextRef context ) CT_AVAILABLE(10_5, 3_2);

介绍了上面的两个.h文件的API之后,还需要介绍CTLine.h的API和CTRun.h文件的API。基本上就够了。这里只介绍上面的两个,如果想了解后两个可以自行查看。类似的方法自己可以分析的

项目需求

图文混排,用CoreText不是一件难事。这样的例子google一下,相信你就会有思路了。或者,如果你的项目支持更高的ios版本的话,可以使用ios7新出的TextKit来完成图文混排,TextKit可是苹果耗费3年时间优化的。可以很好的支持你想要的效果,但是这也不是说苹果将要使用TextKit取代CoreText,TextKit只是CoreText的封装,你可以根据自己的需求,选择合适的框架,来完成自己的工作。

这一次使用CoreText想要完成的效果是,看下图

在一个给定的区域内绘制文本,文本中可能含有很多标签,这些标签可以自定义如下属性

  • borderWidth
  • borderColor
  • borderCornerRadius
  • textFont
  • textColor

附上一段实现上面效果的函数段

// 步骤 1  生成当前的环境

CGContextRef context = UIGraphicsGetCurrentContext();

// 步骤 2  转换坐标系
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

// 步骤 3  生成绘制文字的path
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);

// 步骤 4  组织attributeString,开始渲染
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:@"Hello World! "
                                 " 创建绘制的区域,CoreText 本身支持各种文字排版的区域,"
                                 " 我们这里简单地将 UIView 的整个界面作为排版的区域。"
                                 " 为了加深理解,建议读者将该步骤的代码替换成如下代码,"
                                 " 测试设置不同的绘制区域带来的界面变化。"];

[attString setAttributes:@{
                           NSFontAttributeName:[UIFont systemFontOfSize:20],
                           NSForegroundColorAttributeName:[UIColor redColor],
                           @"addRect":@(YES)
                           } range:[attString.string rangeOfString:@"创建绘制的区域"]];
                           CTParagraphStyleSetting lineBreakMode;
CTLineBreakMode lineBreak = kCTLineBreakByWordWrapping;//kCTLineBreakByCharWrapping;//换行模式
lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode;
lineBreakMode.value = &lineBreak;
lineBreakMode.valueSize = sizeof(CTLineBreakMode);

//组合设置
CTParagraphStyleSetting settings[] = {
    lineBreakMode,
};

//通过设置项产生段落样式对象
CTParagraphStyleRef style = CTParagraphStyleCreate(settings, 1);

[attString addAttributes:@{
                           NSFontAttributeName:[UIFont systemFontOfSize:30],
                           NSForegroundColorAttributeName:[UIColor redColor],
                           @"addRect":@(YES)
                           } range:[attString.string rangeOfString:@"本身支持"]];

[attString addAttributes:@{
                           NSFontAttributeName:[UIFont systemFontOfSize:17],
                           NSForegroundColorAttributeName:[UIColor yellowColor],
                           @"addRect":@(YES),
                           (id)kCTParagraphStyleAttributeName:(id)style,
                           } range:[attString.string rangeOfString:@"我们这里简单地将"]];

CTFramesetterRef framesetter =
CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame =
CTFramesetterCreateFrame(framesetter,
                         CFRangeMake(0, 0), path, NULL);

CFArrayRef lines = CTFrameGetLines(frame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);

for (int i = 0; i < CFArrayGetCount(lines); i++) {
    CTLineRef line = CFArrayGetValueAtIndex(lines, i);
    CGFloat lineAscent;
    CGFloat lineDescent;
    CGFloat lineLeading;
    CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);

    CFArrayRef runs = CTLineGetGlyphRuns(line);

    for (int j = 0; j < CFArrayGetCount(runs); j++) {
        CGFloat runAscent;
        CGFloat runDescent;
        CGPoint lineOrigin = lineOrigins[i];
        CTRunRef run = CFArrayGetValueAtIndex(runs, j);
        NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
        BOOL needAddRect = [[attributes objectForKey:@"addRect"] boolValue];
        //图片渲染逻辑
        if (needAddRect) {                    
            CGRect runRect;
            runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL);
            runRect=CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent);
            /*
             *  这是在文字渲染好了的情况下定义的一种方式,这种方式只是在CTRun加上了一个边框,置于想变动里面文字的排版。
             *  就变得不可能了,这种方式的边框还是有一定的局限性*/
            // 获取需要边框的文字的range
            [[UIColor redColor] set];
            if (runRect.size.width > 4.0f) {
                UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:runRect cornerRadius:1.0f];
                path.lineWidth = 0.5f;
                [path stroke];
            }                    
        }
    }
}
// 步骤 5  绘制
CTFrameDraw(frame, context);        

// 步骤 6  释放已经存在的对象
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);

上面的代码只要随意的复制粘贴到一个UIView的drawRect方法中就可以了。之后将这个UIView的View加载显示的VC中,即可得到如上图所示的效果。

在这里,还是先用一张图来讲述CoreText的排版是如何排版的。

如上图所示:CoreText会把一行里连在一起相同属性的文字合在一起作为一个CTRun,每一行是一个CTLine,多行合在一起组成CTFrame。如上图,第一行的文字有两种样式,第一部分是加粗,第二部分是斜体,因为样式不同所以分成了两个CTRun,CTLine包含了这两个CTRun,CTFrame包含了所有CTLine。

上面效果图的实现与这张理论图很是相似,效果图的实现是在文本排版完成后添加边框来实现的。主要原理是

  • attributedString排版
  • 获取排版中所有的行
  • 获取每行中所有的CTRun
  • 匹配addRect属性
  • 获取CTRun的边框
  • 自定义边框的颜色,圆角,宽度

每个属性相同的文字排版时会使用同一个CTRun来进行排版,利用了这个原理,我们给需要加边框的文字加上了一个addRect属性,这样在检测到这个属性的时候,我们就知道,这个文字是要加上边框的。之后再从这个已经绘制了文本的CTRun对象中取出这个文本在排版时的位置,在这个位置上加上一个边框。就实现上面的效果。

但是这样的后期特效是存在问题的,比如:

  • 边框距离文字太近
  • 边框加粗之后会遮盖文字
  • 边框圆角过大会遮盖文字
  • 文字换行处理不好做

这么多问题,显然不是我想要的效果,所以变换了一下思路。google了一下coreText,看到的基本上都是图文混排的思想,很是懊恼,并没有找到自己想要的。忽然一个想法从脑中穿过,既然图片可以在文本环境中自定义边框展示,那么,文字应该也是可以的。深入的看了一下文字展示的原理

  • 在文字排版的时候为图片预留图片可以展示的空间
  • 排版完成后逐行扫描CTLine,再逐行扫描CTRun
  • 匹配CTRun的属性,成功匹配获取当前CTRun的绘制原点,
  • 获取Image的size,得到图片的path,获取图片资源
  • 绘制图片

如果想给文字预留空间,难点在与AttributeString在展示的时候需要的Size如何获取,因为这个文字最终还是会通过CTRun绘制当当前环境中去的,所以果断前往CTRun.h文件,在这个.h文件中找到了这两个函数

double CTLineGetTypographicBounds(
    CTLineRef line,
    CGFloat * __nullable ascent,
    CGFloat * __nullable descent,
    CGFloat * __nullable leading ) CT_AVAILABLE(10_5, 3_2);

CGRect CTLineGetBoundsWithOptions(
    CTLineRef line,
    CTLineBoundsOptions options ) CT_AVAILABLE(10_8, 6_0);

第一个函数可以知道attributeString在CTLine中所占的高度 高度=ascent+descent,第二个函数可以知道,CTRun的bounds,尽管第二个函数也是可以知道高度,但是这个高度没有前一个函数计算得出的高度精确。知道了上述的信息之后,文本排版的高度和宽度就都已经得到了解决。下面是效果图

与上次不同的是,这次是先将需要绘制的文字的空间预留出来。这样的优点在于可以自定义这个空间的大小,你可以将这个空间的大小放大一些,同时在绘制的时候,在预留的空间里剪裁一个适合的空间来绘制文本。这样边框就不会和文字挨得很紧密了。demo

//为文字设置CTRunDelegate,delegate决定留给文字的空间大小
    CTDisplayViewModel *model = [CTDisplayViewModel new];
    model.font = [UIFont systemFontOfSize:30];
    model.textColor = [UIColor redColor];
    model.boderWidth = 1.0f;
    model.boderColor = [UIColor blueColor];
    model.borderCornerRadius = 2.0f;
    model.text = @"创建绘制的区域";
    [model builder];

    CTRunDelegateCallbacks textCallbacks;
    textCallbacks.version = kCTRunDelegateVersion1;
    textCallbacks.dealloc = RunDelegateDeallocCallback;
    textCallbacks.getAscent = RunDelegateGetAscentCallback;
    textCallbacks.getDescent = RunDelegateGetDescentCallback;
    textCallbacks.getWidth = RunDelegateGetWidthCallback;
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&textCallbacks, (__bridge void * _Nullable)(model));
    // 增加处理文本渲染时的代理
    [attString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id _Nonnull)(runDelegate) range:NSMakeRange(14, 1)];
    [attString addAttribute:@"addRectTag" value:model range:NSMakeRange(14, 1)];

通过代理的方式预留出文字的size,在这个size中你可以适当的放大你需要的尺寸。这样文字在显示的时候就会显得不是那么紧凑,同时空间的大小也会变的可控。

- (void)builder {
    // alloc attributedString
    [self buildAttributtedString];
    // get bounds info
    [self getCTLineRefBoundsInfo];
}

- (void)buildAttributtedString {
    self.attributedString = [[NSMutableAttributedString alloc] initWithString:self.text
                                                                   attributes:@{
                                                                                NSFontAttributeName:self.font,
                                                                                NSForegroundColorAttributeName:self.textColor
                                                                                }];
}

- (void)getCTLineRefBoundsInfo {
    CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)self.attributedString);

    // get bounds info
    CTLineGetTypographicBounds(line, &_lineAscent, &_lineDescent, &_lineLeading);
    _lineBounds = CTLineGetBoundsWithOptions(line,kCTLineBoundsExcludeTypographicLeading);
}

那么在这个delegate函数中你就可以自定义了,你可以自定义你需要增加的值

// delegate
void RunDelegateDeallocCallback( void* refCon ){

}

CGFloat RunDelegateGetAscentCallback( void *refCon ){
    CTDisplayViewModel *model = (__bridge CTDisplayViewModel *)refCon;
    return model.lineAscent;
}

CGFloat RunDelegateGetDescentCallback(void *refCon){
    CTDisplayViewModel *model = (__bridge CTDisplayViewModel *)refCon;
    // 你可以这样
    // return model.lineDescent + 2; 来适配自定义的界面
    return model.lineDescent;
}

CGFloat RunDelegateGetWidthCallback(void *refCon){
    CTDisplayViewModel *model = (__bridge CTDisplayViewModel *)refCon;
    return CGRectGetWidth(model.lineBounds);
}

相对于第一次的实现方案来说,这一次的方案解决了如下问题

  • 边框距离文字太近
  • 边框加粗之后会遮盖文字
  • 边框圆角过大会遮盖文字

但是依然存在的问题还有换行,在这一次的绘制方案上实现换行是不可能的,首先预留的空间是更具CTLine的context做的空间的预留,这个context并没有假设在当前绘制的画布上,所以预留的空间也只是当前绘制文字需要的size而已。但是在预留孔的排版的时候,这个空间需要结合绘制原点才能真正的得到绘制的区域,如果原点+预留空间的width没有超出当前画布的边界的话,那么整体绘制的效果就会满足绘制的需求。但是,这样的情况是存在一定的概率的。

为了解决换行的问题,并保持之前绘制的有点,改变绘制的方案。

  • 遍历attributedString,获取其中的信息和range。

    // 不同属性的attributedString构成了这个遍历数组
    __weak typeof(self) weakSelf = self;
        [self.attributedString enumerateAttributesInRange:NSMakeRange(0, self.attributedString.length)
                                                  options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
                                               usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
                                                   __strong typeof(weakSelf) strongSelf = weakSelf;
                                                   if (strongSelf) [strongSelf createLineRefWithRange:range config:[CTFrameParser configWithAttributes:attrs]];
                                               }];
  • 根据range拿到相同该属性下的attributedString,对这个属性下的文字做排版

    • 计算当前文字的拼接节点是在行开始还是在行中间
      • 行开始的处理
        • 当前需要绘制的文字的长度类型
          • 长度小于当前行的情况,将当前信息收集起来,x游标向后移动
          • 长度大于当前行的情况,计算出本行的size,绘制本行得到FrameRef,根据frameRef拿到当前行绘制了多少文字,截断当前的文字,剩余的文字递归该函数。
      • 行中间的处理
        • 计算当前行是否已经被充满
          • 没有被充满,将当前信息收集起来,x游标向后移动
          • 已经被充满
            • 新来的文字是否需要加上边框
              • 不需要加上边框,计算出本行的size,绘制本行得到FrameRef,根据frameRef拿到当前行绘制了多少文字,截断当前的文字,剩余的文字递归该函数
              • 需要加边框,将之前行文字数组中的文字绘制,x游标指向0,本次文字做递归处理

上面所述的就是本次绘制的核心逻辑,所有的行Path都是经过计算得到的size,所以可以做预留空间。继承绘制方案二的优点,同时也解决了换行的难题

说了这么多,来看一下效果,阶段性的展示

上图存在的问题

  • 坐标系
  • 文字基线不对齐

上图存在的问题

  • 文字基线不对齐

使用

说了那么多究竟该如何使用呢。直接上代码

CTDisplayView *displayView = [CTDisplayView new];
    displayView.frame =CGRectMake(100, 20, CGRectGetWidth(self.view.bounds) - 40, CGRectGetHeight(self.view.bounds) - 200);

    NSString *content = @"阅读分为四个阶段:基础阅读,检视阅读,分析阅读,主题阅读,经典的图书有经典的理由,《如何阅读一本书》的阅读分类方法第一次让我看到自己停留在什么阅读层次,该如何提高。这本书详细给出了每种阅读方法的进行步骤,以及不同种类的书籍要如何阅读,可以说是研究阅读方法的基础教材。看了这本书之后再看其他《越读者》、《王者速读法》等图书强化速读、主题阅读等,阅读方法有了显著的提高。";
    CTFrameParserConfig *config = [CTFrameParserConfig new];

    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:content attributes:[CTFrameParser attributesWithConfig:config]];
    [attributedString addAttributes:@{
                                      NSForegroundColorAttributeName:[UIColor blackColor],
                                      NSFontAttributeName:[UIFont systemFontOfSize:12]
                                      } range:NSMakeRange(0, attributedString.length)];
    [attributedString addAttributes:@{
                                      NSForegroundColorAttributeName:[UIColor redColor],
                                      NSFontAttributeName:[UIFont systemFontOfSize:12],
                                      CTAttributedStringNeedBorder:@(YES)
                                      } range:NSMakeRange(20, 12)];

    [attributedString addAttributes:@{
                                      NSForegroundColorAttributeName:[UIColor redColor],
                                      NSFontAttributeName:[UIFont systemFontOfSize:30],
                                      CTAttributedStringNeedBorder:@(YES)
                                      } range:NSMakeRange(50, 12)];

    [attributedString addAttributes:@{
                                      NSForegroundColorAttributeName:[UIColor redColor],
                                      NSFontAttributeName:[UIFont systemFontOfSize:30],
                                      CTAttributedStringNeedBorder:@(YES),
                                      CTAttributedStringBorderWidth:@(1),
                                      CTAttributedStringBorderColor:[UIColor greenColor],
                                      CTAttributedStringBorderCornerRadius:@(5),
                                      CTAttributedStringBorderHorizonSpacing:@(4),
                                      CTAttributedStringBorderVerticalSpacing:@(6),
                                      } range:NSMakeRange(70, 12)];

    [attributedString addAttributes:@{
                                      NSForegroundColorAttributeName:[UIColor redColor],
                                      NSFontAttributeName:[UIFont systemFontOfSize:12],
                                      CTAttributedStringNeedBorder:@(YES),
                                      CTAttributedStringBorderWidth:@(1),
                                      CTAttributedStringBorderColor:[UIColor yellowColor],
                                      CTAttributedStringBorderCornerRadius:@(2),
                                      CTAttributedStringBorderHorizonSpacing:@(1),
                                      CTAttributedStringBorderVerticalSpacing:@(1),
                                      } range:NSMakeRange(90, 12)];
    displayView.attributedText = attributedString;
    displayView.center = self.view.center;
    displayView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:displayView];

该功能的实现有针对性,如果你想在此基础上添加自己的排版方式,可以fork该工程,添加自己需求。这里主要讲述的还是CoreText排版的思想

Tips

无意之中用xcode8.0-beta4打开了工程,发现文字不能正常展示了。更改了代理中的值如下所示

CGFloat RunDelegateGetAscentCallback( void *refCon ){
    CTDisplayViewModel *model = (__bridge CTDisplayViewModel *)refCon;
    return model.lineAscent + 2;
}

CGFloat RunDelegateGetDescentCallback(void *refCon){
    CTDisplayViewModel *model = (__bridge CTDisplayViewModel *)refCon;
    return model.lineDescent + 2;
}

CGFloat RunDelegateGetWidthCallback(void *refCon){
    CTDisplayViewModel *model = (__bridge CTDisplayViewModel *)refCon;
    return CGRectGetWidth(model.lineBounds) + 2;
}

才可以正常显示,但是想了一下,不能这样去增加值。如果单一的靠增加行布局的空间来实现文本的展示,那整个排版就不可控了。想想这应该是Xcode存在的bug,果断将fontSize增加了50%,果然又不能正常展示了。哎!!!,不知道要给苹果提交几个report。

这个问题在Xcode7.3.1上可以正常运行

在排版的时候,其实可以通过文字之间的非换行连字符,结合文本的换行模式来实现换行的目的。

  • 设置文本的段落排版的换行模式为NSLineBreakByWordWrapping

    // 设置段落样式
    CTParagraphStyleSetting lineBreakMode;
    CTLineBreakMode lineBreak = kCTLineBreakByWordWrapping;//kCTLineBreakByCharWrapping;//换行模式
    lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode;
    lineBreakMode.value = &lineBreak;
    lineBreakMode.valueSize = sizeof(CTLineBreakMode);
    
    //组合设置
    CTParagraphStyleSetting settings[] = {
    lineBreakMode,
    };
    
    //通过设置项产生段落样式对象
    CTParagraphStyleRef style = CTParagraphStyleCreate(settings, 1);
    
    [attString addAttributes:@{
                           NSFontAttributeName:[UIFont systemFontOfSize:17],
                           NSForegroundColorAttributeName:[UIColor yellowColor],
                           @"addRect":@(YES),
                           (id)kCTParagraphStyleAttributeName:(id)style,
                           } range:[attString.string rangeOfString:@"我们这里简单地将"]];

然后修改文字为我\u180E们\u180E这\u180E里\u180E简\u180E单\u180E地\u180E将这样文字在行尾展示补全的时候,就会自动切换到下一行了。但是这里会有一个问题,如下图

文字中有乱七八糟的东西,可以通过把 \u180E 所在字符设置成 [UIColor clearColor] 来避免。

问题到这里你以为会是你想要的效果了,NO。完全不是

  • CTRun的排版中设置了文字我\u180E们\u180E这\u180E里\u180E简\u180E单\u180E地\u180E将,和\u180E
  • CTRun会将这段文字分成一个一个的CTRun
  • 这个时候你在使用的CTRun的时候,你就发现,你加上的边框不是一段文字上,而是一个一个文字上都加上了边框!!!!

呵呵失望了吗!!!!我也郁闷了好久

参考文章

About Core Text

About Text Handling in iOS

Introduction to Core Foundation Design Concepts

CoreText使用教程(一)

Core Text 入门

CoreText使用教程(二)

与剪辑属性返回零高度

段落样子CTParagraphStyle

CoreText Part2