Meet TextKit 2
Swift & UI 高级 26m

认识 TextKit 2

Meet TextKit 2

2021年6月10日

在 Apple 官方观看视频

一句话判断

TextKit 2 是 Apple 对富文本渲染引擎的一次全面重写——基于 NSTextLayoutFragment 的分片布局模型取代了旧的 NSLayoutManager 单一布局模式,性能更好,扩展性更强,但迁移不会是无痛的。

这场 Session 讲了什么

TextKit 是 iOS/macOS 上所有文本渲染的底层框架(UILabelUITextViewNSTextField 都基于它)。TextKit 2 是这个框架自 iOS 7 以来的最大架构变更。

核心概念变化:TextKit 1 使用 NSTextStorage -> NSLayoutManager -> NSTextContainer 的三层架构。TextKit 2 保留了 NSTextStorage,但用 NSTextLayoutFragment 替代了 NSLayoutManager 作为布局单元。

NSTextLayoutFragment 代表文本的一个布局片段(比如一个段落、一个表格单元格)。每个 fragment 可以独立计算布局和渲染,这使得多线程布局成为可能——TextKit 1 的 NSLayoutManager 只能在主线程工作。

Session 还介绍了 NSTextContentManager,它是 NSTextStorage 和布局层之间的协调器。如果你需要渲染非文本内容(比如内嵌图片、公式),可以通过自定义 NSTextContentManager 来实现。

值得深挖的点

为什么 TextKit 1 需要被替换

TextKit 1 的 NSLayoutManager 是一个 monolith(单体)——它管理着整个文本内容的布局。这意味着任何文本变更(哪怕只改了一个字符)都可能触发整个文档的重新排版。对于几百 KB 的大文档,这个开销很明显。

TextKit 2 的 NSTextLayoutFragment 把文档分成小块,只重新排版受影响的那一块。而且 fragment 的布局可以并行处理。这在滚动大文档时尤其明显——TextKit 2 可以只计算可见区域的布局,不需要预先计算整个文档。

与 TextKit 1 的兼容性

UITextView 在 iOS 15 默认仍然使用 TextKit 1。你需要手动启用 TextKit 2。好消息是,两者可以共存——你可以逐步迁移,不需要一次性改写所有文本处理代码。

代码片段

// 启用 TextKit 2
import UIKit

class TextKit2ViewController: UIViewController {
    var textView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let textStorage = NSTextStorage()
        let textContentManager = NSTextContentManager()
        textContentManager.textStorage = textStorage
        
        let textLayoutManager = NSTextLayoutManager()
        textContentManager.addTextLayoutManager(textLayoutManager)
        
        let textContainer = NSTextContainer(size: CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude))
        textLayoutManager.addTextContainer(textContainer)
        
        textView = UITextView(frame: view.bounds)
        // TextKit 2 通过注入新的管理器来启用
        // UITextView 的默认行为仍然是 TextKit 1
    }
}
// 自定义 NSTextLayoutFragment 实现特殊布局
class ParagraphLayoutFragment: NSTextLayoutFragment {
    override var leadingPadding: CGFloat { 16 }
    override var trailingPadding: CGFloat { 16 }
    override var topPadding: CGFloat { 12 }
    override var bottomPadding: CGFloat { 12 }
    
    override func render(at point: CGPoint, in context: NSTextLayoutFragmentDrawingContext) {
        // 自定义渲染逻辑
        // 可以在文本周围绘制装饰线、背景色等
        
        // 先渲染文本内容
        super.render(at: point, in: context)
        
        // 然后渲染自定义装饰
        let bounds = CGRect(origin: point, size: renderingSurfaceSize)
        context.draw { cgContext in
            cgContext.setStrokeColor(UIColor.systemGray4.cgColor)
            cgContext.setLineWidth(1)
            cgContext.move(to: CGPoint(x: bounds.minX + 4, y: bounds.minY))
            cgContext.addLine(to: CGPoint(x: bounds.minX + 4, y: bounds.maxY))
            cgContext.strokePath()
        }
    }
}
// 使用 NSTextElement 自定义文本元素
class ImageTextElement: NSTextElement {
    let image: UIImage
    let size: CGSize
    
    init(image: UIImage, size: CGSize) {
        self.image = image
        self.size = size
        super.init()
    }
    
    // 创建对应的 layout fragment
    override func createLayoutFragment(for textRange: NSTextRange?) -> NSTextLayoutFragment {
        return ImageLayoutFragment(textElement: self, textRange: textRange)
    }
}

class ImageLayoutFragment: NSTextLayoutFragment {
    let imageElement: ImageTextElement
    
    init(textElement: ImageTextElement, textRange: NSTextRange?) {
        self.imageElement = textElement
        super.init(textElement: textElement, textRange: textRange)
    }
    
    override var renderingSurfaceSize: CGSize {
        return imageElement.size
    }
}

最佳实践

不要急于把现有的 TextKit 1 代码迁移到 TextKit 2。iOS 15 的 TextKit 2 实现还有一些限制(比如部分 NSAttributedString 的 attribute 尚未完全支持)。建议只在新建的文本编辑功能中尝试 TextKit 2。

如果你需要做自定义文本布局(比如代码编辑器的高亮、Markdown 渲染),TextKit 2 的 fragment 模型比 TextKit 1 更适合。你可以为每种语法元素(关键字、注释、字符串)创建不同的 NSTextLayoutFragment,分别控制它们的样式。

还有什么值得关注

  • NSTextLayoutManager 支持 textViewportLayoutController 来控制可见区域的布局,这在滚动性能优化中很有用。
  • TextKit 2 的 NSTextSelectionNavigation 提供了更精确的文本选择控制,支持列选择和块选择。
  • Apple 在 macOS 12 的 AppKit 中也引入了 TextKit 2,NSTextView 的行为和 iOS 上的 UITextView 保持一致。
WWDC 2021