阅读 130

初始化 TextKit 的正确方式

首发于公众号

iOS7 之后,苹果推出了用于解决文本排版问题的 TextKit 三件套:

  • NSTextStorage
  • NSLayoutManager
  • NSTextContainer

使用方法比较简单:

[textStorage addLayoutManager:layoutManager];
[layoutManager addTextContainer:textContainer];
复制代码

然后在 view 的 bounds 发生变化的时候调整一下 textContainer 的 size 就可以了。

但是,这里有一处坑,会导致诡异的问题,用下面的方法计算文本需要的 Rect 时,始终会得到 CGRectZero:

[layoutManager usedRectForTextContainer:textContainer];
复制代码

这似乎是 TextKit 的一个 bug(或者是 feature?)。 写代码的时候,顺应着思维,一般都会这么初始化 TextKit:

- (void)initTextKit {
    textStorage = [[NSTextStorage alloc] init];
    layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];
    textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
    [layoutManager addTextContainer:textContainer];
}
复制代码

嗯,根据它们的添加顺序一路写下来,看着很顺畅,这有啥问题? 这就是看不见的坑,textStorage 初始化的时机太早了,顺序应该放到最后,调整后的代码如下:

- (void)initTextKit {
    layoutManager = [[NSLayoutManager alloc] init];
    textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
    [layoutManager addTextContainer:textContainer];
    textStorage = [[NSTextStorage alloc] init];
    [textStorage addLayoutManager:layoutManager];
}
复制代码

addLayoutManager 的调用顺序放在最后,完美解决 usedRectForTextContainer 无法计算的问题。

BTW

Facebook 开源库 ComponentKit 同样有这个问题,在 CKTextKitContext.mm 里:

- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
                           lineBreakMode:(NSLineBreakMode)lineBreakMode
                    maximumNumberOfLines:(NSUInteger)maximumNumberOfLines
                         constrainedSize:(CGSize)constrainedSize
                    layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory
{
if (self = [super init]) {
// Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock.
static std::mutex *__static_mutex = new std::mutex;
    std::lock_guard<std::mutex> l(*__static_mutex);
// Create the TextKit component stack with our default configuration.
    _textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]);
    _layoutManager = layoutManagerFactory ? layoutManagerFactory() : [[NSLayoutManager alloc] init];
    _layoutManager.usesFontLeading = NO;
    [_textStorage addLayoutManager:_layoutManager];
    _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
// We want the text laid out up to the very edges of the container.
    _textContainer.lineFragmentPadding = 0;
    _textContainer.lineBreakMode = lineBreakMode;
    _textContainer.maximumNumberOfLines = maximumNumberOfLines;
    [_layoutManager addTextContainer:_textContainer];
  }
return self;
}
复制代码

不信你可以试一下:

[context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
  CGRect usedRect = [layoutManager usedRectForTextContainer:textContainer];
}];
复制代码

不管字符串的内容是什么, usedRect 的值都是 CGRectZero。

img