WWDC Quick Look 💓 By SwiftGGTeam
Meet TextKit 2

Meet TextKit 2

Watch original video

Highlight

TextKit 2 is Apple’s next-generation text engine, based on three core design principles: correctness (abstracting glyph processing), safety (emphasis on value semantics), and performance (viewport-based layout and rendering).

Core Content

TextKit 1 started with OpenStep, over 20 years old.It drives text display on all Apple platforms, but is constrained by its original design and struggles to integrate well with modern technology.TextKit 2 in Big Sur has quietly powered many of the text components in the system.

TextKit 2 and TextKit 1 coexist, both built on Foundation, Quartz, and Core Text.It retains the MVC design: views have new versions and more components in UIKit/AppKit, model layer and controller layer.

Three core design principles: Correctness abstract glyph processing, Safety emphasize value semantics, and Performance use viewport-based layout and rendering.

Detailed Content

Responsive layout update

32:22

func textViewportLayoutControllerWillLayout(_ controller: NSTextViewportLayoutController) {
    contentLayer.sublayers = nil
    CATransaction.begin()
}

Key points:

  • textViewportLayoutControllerWillLayoutCalled before layout starts
  • Clear the sublayer and start a new CATransaction
  • Prepare for layout updates
func textViewportLayoutController(_ controller: NSTextViewportLayoutController,
                                  configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {
    let (layer, layerIsNew) = findOrCreateLayer(textLayoutFragment)
    if !layerIsNew {
        let oldPosition = layer.position
        layer.updateGeometry()
        if oldPosition != layer.position {
            animate(layer, from: oldPosition, to: layer.position)
        }
    }
    contentLayer.addSublayer(layer)
}

Key points:

  • configureRenderingSurfaceForConfigure rendering surfaces for each layout fragment
  • Can track existing layers and only update position changes
  • Animated position changes to provide a smooth visual experience
func textViewportLayoutControllerDidLayout(_ controller: NSTextViewportLayoutController) {
    CATransaction.commit()
    updateSelectionHighlights()
    updateContentSizeIfNeeded()
    adjustViewportOffsetIfNeeded()
}

Key points:

  • textViewportLayoutControllerDidLayoutCalled after layout is complete
  • Submit CATransaction to update selection highlighting, content size, etc. -Complete layout cycle

Override text attribute display

33:47

func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
    var paragraphWithDisplayAttributes: NSTextParagraph? = nil

    let originalText = textContentStorage.textStorage!.attributedSubstring(from: range)
    if originalText.attribute(.commentDepth, at: 0, effectiveRange: nil) != nil {
        let displayAttributes: [NSAttributedString.Key: AnyObject] = [
            .font: commentFont,
            .foregroundColor: commentColor
        ]
        let textWithDisplayAttributes = NSMutableAttributedString(attributedString: originalText)
        let rangeForDisplayAttributes = NSRange(location: 0, length: textWithDisplayAttributes.length - 2)
        textWithDisplayAttributes.addAttributes(displayAttributes, range: rangeForDisplayAttributes)

        paragraphWithDisplayAttributes = NSTextParagraph(attributedString: textWithDisplayAttributes)
    }
    return paragraphWithDisplayAttributes
}

Key points:

  • textParagraphWithMethod allows overriding display properties without modifying the original text storage
  • Suitable for scenarios where you need to temporarily change the style but do not want to pollute the original data
  • returnniluse the original paragraph when

Hide specific elements

34:06

func textContentManager(_ textContentManager: NSTextContentManager,
                        shouldEnumerate textElement: NSTextElement,
                        with options: NSTextElementProviderEnumerationOptions) -> Bool {
    if !showComments {
        if let paragraph = textElement as? NSTextParagraph {
            let commentDepthValue = paragraph.attributedString.attribute(.commentDepth, at: 0, effectiveRange: nil)
            if commentDepthValue != nil {
                return false
            }
        }
    }
    return true
}

Key points:

  • shouldEnumerateDetermine whether to enumerate specific elements to participate in layout
  • returnfalseLayout and rendering of this element will be skipped
  • Functions such as showing/hiding comments can be realized

Generate custom layout fragments

34:28

func textLayoutManager(_ textLayoutManager: NSTextLayoutManager,
                       textLayoutFragmentFor location: NSTextLocation,
                       in textElement: NSTextElement) -> NSTextLayoutFragment {
    let index = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: location)
    let commentDepthValue = textContentStorage!.textStorage!.attribute(.commentDepth, at: index, effectiveRange: nil) as? NSNumber

    if commentDepthValue != nil {
        let layoutFragment = BubbleLayoutFragment(textElement: textElement, range: textElement.elementRange)
        layoutFragment.commentDepth = commentDepthValue!.uintValue
        return layoutFragment
    } else {
        return NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange)
    }
}

Key points:

  • textLayoutFragmentForAllows returning custom layout fragments for specific locations
  • Different fragment types can be returned based on text attributes
  • Custom fragments can achieve special rendering effects (such as bubbles)

Draw bubble background

34:58

override func draw(at renderingOrigin: CGPoint, in ctx: CGContext) {
    ctx.saveGState()
    let bubblePath = createBubblePath(with: ctx)
    ctx.addPath(bubblePath)
    ctx.setFillColor(bubbleColor.cgColor)
    ctx.fillPath()
    ctx.restoreGState()

    super.draw(at: renderingOrigin, in: ctx)
}

Key points:

  • In customizationNSTextLayoutFragmentOverriding in subclassesdrawmethod
  • First draw a custom background (such as bubbles) and then callsuper.drawdraw text
  • useCGPath(roundedRect:cornerWidth:cornerHeight:)Create a rounded rectangular path

Switch NSTextView to TextKit 2

37:26

var scrollView: NSScrollView!
var containerSize = CGSize.zero
var textContainer = NSTextContainer()
var textContentStorage = NSTextContentStorage()

override func viewDidLoad() {
    super.viewDidLoad()

    scrollView = NSScrollView(frame: view.bounds)
    view.addSubview(scrollView)

    setUpScrollView(scrollsHorizontally: false)
}

func setUpTextContainer(scrollsHorizontally: Bool) {
    let contentSize = scrollView.contentSize
    if scrollsHorizontally {
        containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        textContainer.containerSize = containerSize
        textContainer.widthTracksTextView = false
    } else {
        containerSize = NSSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
        textContainer.containerSize = containerSize
        textContainer.widthTracksTextView = true
    }
}

func setUpTextView(scrollsHorizontally: Bool) {
    let textLayoutManager = NSTextLayoutManager()
    textLayoutManager.textContainer = textContainer

    textContentStorage.addTextLayoutManager(textLayoutManager)

    let textView = NSTextView(frame: scrollView.contentView.bounds, textContainer: textLayoutManager.textContainer)

    textView.isEditable = true
    textView.isSelectable = true
    textView.textStorage?.append(NSAttributedString(string: "Text content..."))
    scrollView.documentView = textView
}

Key points:

  • createNSTextLayoutManagerand associated toNSTextContainer
  • Add layout manager toNSTextContentStorage
  • using layout managertextContainerinitializationNSTextView
  • This will make the text view use TextKit 2 for layout and rendering

Core Takeaways

1. Add bubble comment function to text editor

If your app has text editing capabilities (such as code editor, document collaboration), add a bubble comment function similar to code review.

Implementation idea: Create customization for commentsNSTextLayoutFragmentsubcategories (e.g.BubbleLayoutFragment),existtextLayoutFragmentForDetect comment attributes and return custom fragment.rewritedrawMethod to draw bubble background.

2. Implement incremental rendering of syntax highlighting

The code editor’s syntax highlighting needs to be updated frequently.TextKit 2’s viewport-based layout can redraw only the visible area.

Implementation idea: useNSTextViewportLayoutControllerMonitor changes in the visible area and apply highlight styles only to the text fragments within the visible range.CooperateconfigureRenderingSurfaceForCaching rendering results.

3. Implement special layout for chat messages

Messages in chat apps usually require different layouts (your own message to the right, the other party’s message to the right, possibly with bubbles and avatars).

Implementation idea: create for each messageNSUserActivity,existtextLayoutFragmentForReturns different custom fragments based on the sender.Custom snippets can control background, rounded corners, avatar position, etc.

4. Optimize the scrolling performance of large documents

TextKit 1 may freeze when displaying large documents with hundreds of thousands of lines.TextKit 2’s viewport layout only handles visible content.

Implementation ideas: Make sure to useNSTextLayoutManagerinstead of the old oneNSLayoutManager.forNSTextContainerSet up a reasonablecontainerSize, to avoid performance problems caused by infinite width.

5. Implement a collapsible text area

Markdown editors and code editors often need to collapse code blocks or chapters.TextKit 2 can do this by hiding elements.

Implementation idea: inshouldEnumerateCheck the folding status and return the folded elementfalse.Called when updating the folded statetextLayoutManager.textViewportLayoutController.layoutViewportRe-layout.

Comments

GitHub Issues · utterances