WWDC.ai

Elevate your app's text experience with TextKit

Use new TextKit hooks to extend UITextView and NSTextView with viewport-aware rendering, collapsible layout, and reusable text attachments.

Watch on Apple Developer

TL;DR

  • TextKit's four layers are storage, layout, viewport, and view; the viewport layout process is the key extension point for visible text rendering.
  • UITextView and NSTextView now publicly conform to NSTextViewportLayoutControllerDelegate, enabling subclasses to override viewport layout callbacks while keeping built-in editing behavior.
  • New rendering surface APIs let custom TextKit views associate layout fragments with drawable views or layers across layout cycles.
  • UITextView can register text attachment view provider reuse policies to preserve attachment state during inline edits or viewport scrolling.

Why TextKit matters for rich text experiences

TextKit powers text layout and rendering across SwiftUI, UIKit, and AppKit text controls. Developers typically choose between framework text views, which provide editing behavior such as input, selection, accessibility, undo, dictation, and inline predictions, or fully custom TextKit views, which provide low-level control but require much more implementation work.

The new APIs are aimed at reducing that tradeoff: start from UITextView or NSTextView when possible, then extend their TextKit viewport behavior where custom rendering or layout metadata is needed.

  • Use UITextView on UIKit and NSTextView on AppKit for a full-featured editing base.
  • Use SwiftUI TextEditor for the most convenient SwiftUI long-form editing path, or wrap UITextView/NSTextView with ViewRepresentable when TextKit hooks are needed.
  • Build a fully custom TextKit view when the app needs direct control over storage, layout, viewport coordination, and rendering surfaces.

TextKit architecture recap

TextKit is organized into four layers: text storage, layout, viewport, and view. The storage and layout layers break attributed text into text elements and produce layout fragments; the viewport layer determines which fragments are visible; the view layer renders those fragments into UI framework views or layers.

For an NSAttributedString-backed document, NSTextContentStorage creates NSTextParagraph elements, and NSTextLayoutManager creates immutable NSTextLayoutFragment objects with calculated layout information. Editing a paragraph recreates the paragraph and its corresponding layout fragment.

NSTextViewportLayoutController coordinates with the text layout manager and text view whenever the viewport changes due to scrolling, editing, or selection. It asks for layout fragments intersecting the viewport and sends them to its delegate for rendering.

  • NSTextContentStorage and NSTextParagraph are concrete NSAttributedString-based storage types.
  • NSTextContentManager and NSTextElement are the abstract types to subclass for nonstandard backing stores.
  • Multiple NSTextLayoutManager instances can be connected to the same NSTextContentStorage to present synchronized views of the same document.

New rendering surface APIs for custom TextKit views

NSTextViewportRenderingSurface is a new protocol for drawable visual elements inside a viewport. UIView, NSView, or CALayer types can conform so TextKit code can reason about the surfaces used to render layout fragments.

NSTextViewportRenderingSurfaceKey identifies a rendering surface across viewport layout cycles. NSTextLayoutFragment can be used as a key, making it practical to cache or look up rendering surfaces with map tables or dictionaries during viewport layout.

  • Assign a rendering surface for a key during the viewport layout process with the delegate API.
  • Mappings are cleared at the beginning of a viewport layout process.
  • Query a rendering surface for a key during didLayout with the viewport controller's renderingSurfaceFor method.

Rendering surface conformance

A UIView subclass can opt in as a TextKit viewport rendering surface.

class MyView: UIView, NSTextViewportRenderingSurface {}

Caching surfaces by layout fragment

NSTextLayoutFragment can act as a rendering surface key for cache lookups across viewport layout work.

class MyView: UIView, NSTextViewportRenderingSurface {}
var cache: NSMapTable<NSTextLayoutFragment, MyView>

Extending UITextView and NSTextView

UITextView and NSTextView now publicly conform to NSTextViewportLayoutControllerDelegate. Subclasses can override the viewport layout delegate callbacks to add behavior before layout, while configuring each layout fragment, and after layout completes.

The important pattern is to call super in each override so the framework text view keeps its default behavior, then add app-specific collection, rendering, or layout invalidation logic.

  • Override willLayout for setup before a viewport layout pass.
  • Override configureRenderingSurfaceFor to inspect or configure each visible NSTextLayoutFragment.
  • Override didLayout to consume accumulated information after visible fragments have been processed.
  • Use ViewRepresentable to embed UITextView or NSTextView in SwiftUI when TextEditor is not enough.

Wrap framework text views for SwiftUI

SwiftUI apps can host NSTextView or UITextView directly when they need TextKit-specific hooks.

import SwiftUI

struct MyTextView: View {
    var body: some View {
        TextViewRepresentable()
    }
}

#if os(macOS)
struct TextViewRepresentable: NSViewRepresentable {
    func makeNSView(context: Context) -> NSTextView { NSTextView() }
    func updateNSView(_ nsView: NSTextView, context: Context) {}
}
#else
struct TextViewRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> UITextView { UITextView() }
    func updateUIView(_ uiView: UITextView, context: Context) {}
}
#endif

Viewport delegate override points

The three delegate methods provide the core extension points for framework text views.

class TextView: UITextView {
    override func textViewportLayoutControllerWillLayout(
        _ textViewportLayoutController: NSTextViewportLayoutController
    ) {
        super.textViewportLayoutControllerWillLayout(textViewportLayoutController)
        // Set up state for this layout pass.
    }

    override func textViewportLayoutController(
        _ textViewportLayoutController: NSTextViewportLayoutController,
        configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment
    ) {
        super.textViewportLayoutController(
            textViewportLayoutController,
            configureRenderingSurfaceFor: textLayoutFragment
        )
        // Inspect or collect information for this visible fragment.
    }

    override func textViewportLayoutControllerDidLayout(
        _ textViewportLayoutController: NSTextViewportLayoutController
    ) {
        super.textViewportLayoutControllerDidLayout(textViewportLayoutController)
        // Publish accumulated information.
    }
}

Example patterns: line numbers and collapsible sections

For a code editor, a UITextView subclass can use viewport callbacks to collect paragraph frames for visible layout fragments, calculate the first visible line number, and send that information to a container view that draws a line-number gutter.

For collapsible sections, the text view can combine viewport delegate callbacks with NSTextContentStorageDelegate. The delegate method textContentManager(_:shouldEnumerate:options:) can skip layout for collapsed text elements, while a set of paragraph offsets tracks which sections are collapsed.

  • Compute the starting line number by enumerating text elements from the document start until the viewport range.
  • Collect each fragment's layoutFragmentFrame during configureRenderingSurfaceFor.
  • Convert fragment frames from text container coordinates to viewport coordinates before drawing external decorations.
  • After toggling collapse state, ask the viewport layout controller delegate to mark layout as needed.

Compute the starting line number for the viewport

The sample counts paragraphs before the viewport; the session notes the sample code improves this with caching.

func startingLineNumber(for viewportRange: NSTextRange?) -> Int {
    guard let viewportRange,
          let storage = textLayoutManager?.textContentManager as? NSTextContentStorage
    else { return 0 }

    let startLocation = storage.documentRange.location
    var count = 1

    storage.enumerateTextElements(from: startLocation) { element in
        guard let range = element.elementRange else { return true }
        if range.location.compare(viewportRange.location) != .orderedAscending {
            return false
        }
        count += 1
        return true
    }

    return count
}

Collapse state and layout invalidation

Collapsed sections are represented as paragraph offsets, and toggling a section forces the viewport to relayout.

class TextView: UITextView, NSTextContentStorageDelegate {
    var collapsedSections: Set<Int> = []

    func textContentManager(
        shouldEnumerate textElement: NSTextElement,
        options: NSTextContentManager.EnumerationOptions
    ) -> Bool {
        // Return false for collapsed paragraphs.
    }

    func toggleSection(headerOffset: Int) {
        if collapsedSections.contains(headerOffset) {
            collapsedSections.remove(headerOffset)
        } else {
            collapsedSections.insert(headerOffset)
        }

        guard let textLayoutManager else { return }
        let controller = textLayoutManager.textViewportLayoutController
        controller.delegate?.textViewportLayoutControllerReceivedSetNeedsLayout?(controller)
    }
}

Text attachment view provider reuse

Text attachments are stored in text storage as NSTextAttachment objects. During layout, TextKit asks for an NSTextAttachmentViewProvider, which supplies the information needed to render the attachment inside the text view.

Because text layout objects are immutable, edits in a paragraph can otherwise recreate attachment view providers and reset state, such as an inline animation. UITextView now supports registering reuse policies for a particular NSTextAttachmentViewProvider subclass.

  • Use onEditingInlineParagraphs to preserve a view provider across edits in the same paragraph.
  • Use onScrollingOutOfViewport to cache an attachment rendering surface when it leaves the viewport and restore it later.
  • Reuse policies can be combined depending on the attachment's behavior and state requirements.

Register attachment view provider reuse policy

Registering a policy for the provider subclass lets UITextView preserve provider state for matching attachments.

class ViewController: UIViewController {
    var textView: UITextView!

    func setupTextView() {
        textView = UITextView()
        textView.register(
            [.onEditingInlineParagraphs],
            forTextAttachmentViewProviderType: AnimatedAttachmentViewProvider.self
        )
    }
}

Resources

Unofficial, not associated with Apple. BySuperwall

On this page

Ask AI