Contents

Managing viewport layout and attachment reuse in text views

Customize layout and preserve attachment views in your text view subclass.

Overview

If you build a text editor with rich attachments — like images, videos, or interactive controls — you may notice that attachment views flicker or lose their state when someone scrolls the attachment out of the viewport, or types in the same paragraph. This is because text views discard and recreate attachment views as part of their normal layout process.

UITextView and NSTextView conform to NSTextViewportLayoutControllerDelegate, giving your subclass direct access to the layout process. You can override the delegate methods to respond to layout events and customize how text fragments appear.

You can also register reuse policies to tell the text view which attachment views to preserve across scrolling and editing, eliminating flicker and preserving states like focus and playback position.

Override viewport layout methods

layoutViewport() encompasses the viewport layout process. Each time the visual state within the viewport changes, TextKit calls it. During each viewport layout process, NSTextViewportLayoutController calls the text view’s delegate methods at three points — before it starts, before each text layout fragment is about to be rendered, and after it completes. Override any of these methods in your UITextView (or NSTextView in macOS) subclass to customize what happens at each stage:

class CustomTextView: UITextView {

    override func textViewportLayoutControllerWillLayout(
        _ textViewportLayoutController: NSTextViewportLayoutController
    ) {
        super.textViewportLayoutControllerWillLayout(textViewportLayoutController)
        // Prepare any state before the layout pass begins.
    }

    override func textViewportLayoutController(
        _ textViewportLayoutController: NSTextViewportLayoutController,
        configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment
    ) {
        super.textViewportLayoutController(textViewportLayoutController, configureRenderingSurfaceFor: textLayoutFragment)
        // Inspect or customize each laid-out text fragment.
    }

    override func textViewportLayoutControllerDidLayout(
        _ textViewportLayoutController: NSTextViewportLayoutController
    ) {
        super.textViewportLayoutControllerDidLayout(textViewportLayoutController)
        // Finalize layout after all fragments are positioned.
    }
}

The following table shows the set of available override points:

Method

When called

Textviewportlayoutcontrollerwilllayout(_:)

Before each layout pass begins

Textviewportlayoutcontroller(_:configurerenderingsurfacefor:)

Once per laid-out fragment, with its rendering surface

Textviewportlayoutcontrollerdidlayout(_:)

After each layout pass completes

Textviewportlayoutcontrollerreceivedsetneedslayout(_:)

When the viewport requests a layout invalidation

Viewportbounds(for:)

To query the current viewport bounds

Register reuse policies

Use the registration API to preserve attachment views across layout passes. Call register(_:forTextAttachmentViewProviderType:) once during setup to declare the reuse behavior per provider type:

// Retain video attachment views across both scrolling and paragraph edits.
textView.register(
    [.onScrollingOutOfViewport, .onEditingInlineParagraphs],
    forTextAttachmentViewProviderType: VideoAttachmentViewProvider.self
)

// Retain drawing views only when they scroll out of view.
textView.register(
    [.onScrollingOutOfViewport],
    forTextAttachmentViewProviderType: DrawingAttachmentViewProvider.self
)

After you register a provider type, the text view holds on to those attachment views when scrolling moves them out of view or editing changes the surrounding paragraph, and restores them when they come back. This automatically keeps video playback position and focused controls intact across scrolling and paragraph edits.

See Also

Layout