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
flowchart LR
Window["WindowController + toolbar"] --> VC["DocumentViewController"]
VC --> Model["DocumentViewModel"]
VC --> View["DocumentView"]
Model --> TextKit["NSTextContentStorage + NSTextLayoutManager"]
View --> TextKit
View --> Coordinator["NSWritingToolsCoordinator"]
Coordinator --> View
View --> FragmentViews["TextLayoutFragmentView + selection view"]
Reference code
WritingToolsSampleApp/Views/DocumentViewController.swift:10 — the controller constructs and connects the model and custom view.
class DocumentViewController: NSViewController {
// ...
}Ownership and state
classDiagram
DocumentViewController *-- DocumentViewModel : creates
DocumentViewController *-- DocumentView : creates
DocumentViewModel *-- NSTextContentStorage
DocumentViewModel *-- NSTextLayoutManager
DocumentView --> ViewModelDelegate : weak callback target
DocumentView *-- NSWritingToolsCoordinator : configures
DocumentView *-- NSMapTable : fragment view map
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
DocumentViewandDocumentViewModel;+Capabilityfilenames state extension purpose. - Protocols and delegates use role suffixes:
ViewModelDelegateand framework...Delegateconformances. - Commands are explicit about coordinate domains, such as
replaceText(inRange:)andadjustRange(_: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 |