Sample CodemacOSReviewed 2026-07-21View on Apple Developer

Enhancing your custom text engine with Writing Tools

At a glance

Item Summary
Purpose Integrate Writing Tools into a custom TextKit 2 editor rather than an NSTextView.
Architecture DocumentViewController composes a text-focused view model with a custom NSView; capability extensions implement editing, selection, pasteboard, and Writing Tools callbacks.
Critical boundary DocumentView translates Writing Tools context-relative ranges and animation requests into document-model operations and rendered geometry.

Project structure

WritingToolsSampleApp/
├── ViewModels/
│   ├── DocumentViewModel.swift
│   └── DocumentViewModel+{Input,Selection,Formatting,Pasteboard}.swift
└── Views/
    ├── DocumentViewController.swift
    ├── WindowController.swift
    └── DocumentView/
        ├── DocumentView.swift
        ├── DocumentView+Editing.swift
        └── DocumentView+WritingToolsCoordinator.swift

Files are split by capability around two central types; this keeps protocol-heavy framework integration navigable without introducing extra service layers.

Overall architecture

Reference code

WritingToolsSampleApp/Views/DocumentViewController.swift:10 — the controller constructs and connects the model and custom view.

class DocumentViewController: NSViewController {
    // ...
}

Ownership and state

Ownership evidence

WritingToolsSampleApp/ViewModels/DocumentViewModel.swift:10 — the model owns the TextKit graph and exposes a weak presentation callback.

class DocumentViewModel: NSObject {
    // ...
}

The controller owns the feature lifetime. The view model is the sole text-storage mutation boundary. The view owns transient viewport, selection, Writing Tools context, range, animation-overlay, and fragment-view state.

Class and protocol design

Type Responsibility Contracts
DocumentViewModel Text storage, selection, formatting, input, and pasteboard primitives Local ViewModelDelegate notification
DocumentView Custom rendering, input, viewport layout, selection visuals, and Writing Tools adaptation NSTextViewportLayoutControllerDelegate, NSWritingToolsCoordinator.Delegate, ViewModelDelegate
DocumentViewController Compose the editor and route toolbar commands NSToolbarItemValidation
WindowController Build toolbar items and target them at the content controller NSToolbarDelegate

The local protocol is intentionally small: the view model reports only text and selection changes. Framework-specific range and animation methods stay on DocumentView.

Access control

Boundary Effect and rationale
private scroll view and document view Keeps composition mutations inside DocumentViewController.
private fragment helpers, observers, constraints, and selection view Protects viewport/rendering invariants inside DocumentView.
private formatter or geometry helpers in extensions Prevents framework delegate methods from becoming a general app API.
Implicit internal editor types and protocol Allows capability extensions across files while keeping the implementation app-local.
@preconcurrency Adjusts concurrency checking for imported delegate contracts; it is not an access modifier.

WritingToolsSampleApp/Views/DocumentView/DocumentView.swift:28 shows the private transient rendering state.

Logic ownership and placement

Logic Owner
Text mutations and selection calculations DocumentViewModel and its capability extensions
Viewport fragment reuse and drawing DocumentView
Context-range translation and Writing Tools previews DocumentView+WritingToolsCoordinator
Scroll-view and initial-document composition DocumentViewController
Toolbar item creation WindowController

Design patterns

Pattern Evidence Purpose
View model WritingToolsSampleApp/ViewModels/DocumentViewModel.swift:10 Separates mutable text semantics from rendering.
Delegate WritingToolsSampleApp/ViewModels/DocumentViewModel.swift:70 Reports model changes without owning the view.
Adapter WritingToolsSampleApp/Views/DocumentView/DocumentView+WritingToolsCoordinator.swift:10 Converts Writing Tools callbacks into model operations and view geometry.
Capability extensions WritingToolsSampleApp/ViewModels/DocumentViewModel+Selection.swift:10 Groups a large type by behavior without fake object boundaries.
Weak view cache WritingToolsSampleApp/Views/DocumentView/DocumentView.swift:34 Maps TextKit fragments to rendering views without making the map their lifetime owner.

Naming conventions

  • The central types use DocumentView and DocumentViewModel; +Capability filenames state extension purpose.
  • Protocols and delegates use role suffixes: ViewModelDelegate and framework ...Delegate conformances.
  • Commands are explicit about coordinate domains, such as replaceText(inRange:) and adjustRange(_:forContext:).

Architecture takeaways

  • Put text mutations behind one model even when several framework delegates can initiate them.
  • Treat Writing Tools as an adapter at the custom-view boundary.
  • Keep persistent document state separate from ephemeral overlay and animation state.
  • Capability-based files can be clearer than creating many one-method wrapper classes.

Source map

Source Role
WritingToolsSampleApp/ViewModels/DocumentViewModel.swift:10 TextKit model owner
WritingToolsSampleApp/Views/DocumentView/DocumentView.swift:10 Custom renderer and viewport delegate
WritingToolsSampleApp/Views/DocumentView/DocumentView+WritingToolsCoordinator.swift:10 Writing Tools adapter
WritingToolsSampleApp/Views/DocumentViewController.swift:10 Feature composition
WritingToolsSampleApp/Views/WindowController.swift:19 Toolbar integration