Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

TextKit 2 on macOS: Viewport Layout, Text Ranges, and Fragment Rendering

Use TextKit 2 to keep large AppKit documents responsive with viewport-scoped layout, stable text locations, fragment drawing, and measurable invariants.

TextKit 2 is AppKit’s modern text layout architecture for editors and custom text displays. Its central shift is that applications can reason about text locations, ranges, layout fragments, and a viewport instead of forcing every character in a long document into a fully materialized legacy glyph layout. It does not make text rendering automatic: the view still has to coordinate content storage, layout, viewport movement, selection, invalidation, and accessibility.

For a conventional editable document, begin with NSTextView configured to use its modern text layout manager. Build a custom text surface only when the product needs a presentation the standard text view cannot provide. If you do build one, keep the text model independent from view coordinates and treat layout as a cache derived from a particular content revision and viewport.

Know the two layout paths

NSTextView exposes both the newer textLayoutManager path and the older layoutManager path. The older TextKit 1 stack remains present for compatibility, but accessing it can cause the view to use the legacy engine. Do not casually mix NSLayoutManager glyph APIs into a TextKit 2 design and then assume viewport behavior remains intact. When migrating existing editor code, identify each call site that forces legacy layout and decide whether it can be expressed through TextKit 2 ranges and fragments instead.

A simple setup starts with the standard view:

import AppKit

@MainActor
func makeTextView() -> NSTextView {
    let view = NSTextView(usingTextLayoutManager: true)
    view.isEditable = true
    view.isSelectable = true
    view.textContainer?.widthTracksTextView = true
    view.textContainer?.heightTracksTextView = false
    return view
}

This is an AppKit text view, not a complete editor configuration. In a document app, connect it to the scroll view and the document’s attributed content through the supported text-view APIs. Keep ownership of the model and save lifecycle outside the rendering callback. Avoid building a separate mutable attributed string in the view that can diverge from the document’s actual source.

Viewport-scoped layout

The viewport layout controller coordinates layout in the visible region plus an overdraw area. As the user scrolls, the active range can move. A custom view should ask the controller to lay out the current viewport when its bounds or text content changes, and its delegate should create, position, or recycle rendering surfaces for the fragments that are now relevant.

Do not equate “not laid out yet” with “there is no text.” A distant range may not have fragments until the viewport reaches it. Conversely, drawing every fragment in the document defeats the reason to use viewport layout. Keep viewport work bounded: lay out what is visible and a modest prefetch margin, then discard or reuse offscreen view-backed attachments according to their identity and lifecycle.

When implementing a custom viewport, distinguish the document’s logical range from a fragment’s rectangle. A text location is meaningful within the content manager; a view-space rectangle depends on line wrapping, container width, writing direction, insets, and the current viewport. Recompute geometry after edits and layout-affecting changes. Do not serialize a fragment frame or use it as a stable annotation anchor.

TextKit 2’s range APIs use NSTextLocation and NSTextRange, not raw glyph numbers. Store durable annotations as model positions that can survive re-layout, such as an application-level anchor or a validated text offset with context. A selection can be represented by a range in the current content revision, but a persisted comment needs a migration strategy if the underlying text changes.

Fragments and drawing responsibility

NSTextLayoutFragment is a layout unit that typically corresponds to a renderable region. It can expose line fragments and geometry that a custom surface can draw. The layout manager computes text layout; the app decides how fragments map to views or layers and when those renderers are reused. A fragment is not necessarily one visual line, and a line fragment is not a model record. Never assume a specific one-to-one relationship among paragraphs, fragments, and display objects.

When drawing, keep coordinate conversion explicit. TextKit geometry is associated with the text container and its layout coordinate system, while an AppKit view can have its own flipped orientation, scroll offset, and insets. Apply each transform once, then test selection rectangles, caret placement, pointer hit testing, and drawing using the same conversion path. A renderer that looks correct but maps clicks to neighboring characters is not correct.

Do not run expensive full-document layout synchronously during every keystroke. Coalesce invalidations, process model edits on the editor’s owning actor, and let the layout machinery update affected regions. If your own asynchronous work produces decorations or syntax results, tag it with the content revision and discard results for older revisions. Stale highlighting can be as confusing as stale text.

Attachments and non-text objects

Text attachments introduce a second lifecycle: an attachment can have a model representation, a layout size, and a view or drawing representation. The document model should own the attachment identity and data. A visible attachment renderer may be recycled when the viewport changes, so do not make the renderer itself the only owner of unsaved content.

Measure attachment dimensions before they influence line layout. If loading an image or preview is asynchronous, first reserve a predictable size or define how layout changes when the final size arrives. When the result comes back, verify that the attachment still exists at the same logical location and that the document revision is current. Large media should not be decoded on the main thread merely because a text view asks for a line fragment.

If the editor supports custom text elements, ensure their accessibility, selection, copy/paste, and keyboard semantics are defined alongside their visual appearance. A layout fragment can paint pixels; it does not automatically make a custom object navigable or understandable to assistive technology.

Editing, selection, and legacy interoperation

TextKit 2 can represent selections and navigation without requiring the app to treat glyph indices as document offsets. Preserve Unicode correctness. A user-perceived character may contain multiple Unicode scalars, and a shaping engine can map character sequences to glyph runs in ways that do not preserve a one-character/one-glyph assumption. When bridging to APIs that require NSRange, document that the range is measured in UTF-16 units and keep conversions at explicit boundaries.

Incremental adoption is possible, but it needs tests around the interfaces between old and new code. An extension or plug-in may access the legacy layoutManager property, unexpectedly triggering the old path. Search the codebase for legacy layout access, glyph enumeration, and direct NSLayoutManager calls. Isolate unavoidable compatibility code behind a small adapter and verify that both paths produce consistent selection and pagination for supported documents.

Do not assume TextKit 2 is a drop-in performance switch for every document. A custom layout, huge attachment, expensive attribute provider, or synchronous syntax pass can still block input. Measure model mutation, layout, fragment drawing, attachment work, and view updates separately. Test both a short document and a long file with realistic mixed scripts.

Failure modes and diagnostics

Symptoms such as a blank area after scrolling, incorrect selection after resize, disappearing attachment views, or a legacy code path taking over usually indicate mismatched lifecycle assumptions. Log a document revision, viewport bounds, text range, fragment count, container width, and layout duration. Avoid logging document text. If only the lower half of the screen is blank, compare the controller’s reported viewport and range with the view’s scroll bounds before adding arbitrary extra layout calls.

Test cold open, rapid typing, paste of a large block, undo/redo, window resize, zoom or font change, attachment completion, scroll to an unlaid-out range, and reopening a saved document. Include right-to-left text, combining marks, emoji sequences, long unbroken strings, and mixed scripts. Assert that a viewport update eventually yields fragments for visible text, selection ranges stay inside valid content, and obsolete asynchronous decorations never win over a newer revision.

Acceptance criteria

Define performance targets on representative hardware rather than asserting that a framework choice guarantees speed. Measure first visible text, keystroke-to-render latency, scroll hitching, memory growth during long-document navigation, and the number of active attachment renderers. Verify that a document of a fixed size does not create an unbounded number of live view objects merely because a user scrolled through it once.

TextKit 2 gives an editor a modern layout model with explicit ranges and viewport participation. The production work is to keep document state canonical, avoid accidental legacy-engine access, make coordinate transforms reproducible, and prove that visible content stays correct through edits and scrolling.

Related:

Sources:

Comments