Using TextKit 2 to interact with text
At a glance
| Item | Summary |
|---|---|
| Purpose | Build a custom TextKit 2 viewport renderer with selection, nested comments, and shared iOS/macOS layout fragments. |
| App architecture | Each platform controller constructs NSTextContentStorage, NSTextLayoutManager, and NSTextContainer; its custom TextDocumentView becomes the layout/viewport delegate, maps fragments to layers, and substitutes shared BubbleLayoutFragment instances for comment paragraphs. |
| Main patterns | Explicit TextKit object graph, Viewport rendering adapter, Layout fragment specialization, Parallel platform shells |
| Project style | Fifteen Swift files split into iOS, macOS, and Shared groups; platform controllers/views surround shared fragment and reaction code. |
Project structure
Source bundle/
├── iOS/
│ ├── AppDelegate.swift
│ ├── TextDocumentView.swift
│ ├── CommentPopoverViewController.swift
│ ├── SceneDelegate.swift
│ └── TextDocumentViewController.swift
├── macOS/
│ ├── AppDelegate.swift
│ ├── TextDocumentView.swift
│ ├── CommentPopoverViewController.swift
│ ├── TextDocumentViewController.swift
│ └── WindowController.swift
└── Shared/
├── BubbleLayoutFragment.swift
└── CommentUtils.swift
Structure observations
- The TextKit graph is assembled explicitly in each platform controller rather than hidden in app delegates.
- The custom document view is both the viewport rendering adapter and interaction surface for selection/comments.
- Shared fragment/layer types contain drawing geometry; iOS and macOS files contain platform input, scrolling, and presentation behavior.
Overall architecture
flowchart LR
IOS["iOS TextDocumentViewController"] --> GRAPH["NSTextContentStorage → NSTextLayoutManager → NSTextContainer"]
MAC["macOS TextDocumentViewController"] --> GRAPH
GRAPH --> VIEW["Platform TextDocumentView"]
VIEW --> LMDELEGATE["NSTextLayoutManagerDelegate"]
LMDELEGATE --> NORMAL["NSTextLayoutFragment"]
LMDELEGATE --> BUBBLE["BubbleLayoutFragment for comments"]
NORMAL --> LAYER["TextLayoutFragmentLayer"]
BUBBLE --> LAYER
LAYER --> SURFACE["Viewport content CALayer"]
VIEW --> EDIT["Selection and comment editing transaction"]
Reference code
iOS/TextDocumentViewController.swift:29 — explicit TextKit 2 object graph
required init?(coder: NSCoder) {
textLayoutManager = NSTextLayoutManager()
textContentStorage = NSTextContentStorage()
super.init(coder: coder)
textContentStorage.delegate = self
textContentStorage.addTextLayoutManager(textLayoutManager)
let textContainer = NSTextContainer(size: CGSize(width: 200, height: 0))
textLayoutManager.textContainer = textContainer
}Interpretation
Text storage, layout, and rendering are distinct owners. The controller assembles content/layout services and supplies display attributes; the custom view owns viewport and interaction mechanics. Shared fragment subclasses and CALayers turn TextKit layout results into specialized drawing without making controllers render text directly.
Ownership and state
classDiagram
TextDocumentViewController *-- NSTextContentStorage : private
TextDocumentViewController *-- NSTextLayoutManager : private
NSTextContentStorage o-- NSTextLayoutManager : registered manager
NSTextLayoutManager *-- NSTextContainer : assigned
TextDocumentView o-- NSTextLayoutManager : assigned collaborator
TextDocumentView *-- CALayer : content and selection
TextDocumentView *-- NSMapTable : weak fragment-layer map
CALayer *-- TextLayoutFragmentLayer : viewport sublayers
TextLayoutFragmentLayer o-- NSTextLayoutFragment : draws
BubbleLayoutFragment --|> NSTextLayoutFragment
Ownership evidence
iOS/TextDocumentView.swift:141 — view-local rendering state and delegates
private var contentLayer: CALayer! = nil
private var selectionLayer: CALayer! = nil
private var fragmentLayerMap: NSMapTable<NSTextLayoutFragment, CALayer>
private var padding: CGFloat = 5.0
var textLayoutManager: NSTextLayoutManager? {
didSet {
textLayoutManager?.delegate = self
textLayoutManager?.textViewportLayoutController.delegate = self
layer.setNeedsLayout()
}
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
Platform TextDocumentViewController |
Content storage and layout manager | Creates private instances during initialization and connects them | It loads source text, controls comment visibility, and supplies paragraph display attributes. |
NSTextContentStorage / NSTextLayoutManager |
Text elements, layout, container, selections | Linked through TextKit’s manager relationship | TextKit drives layout and editing transactions. |
Platform TextDocumentView |
Content/selection layers, weak fragment-layer map, padding | Creates and privately retains rendering state | It manages viewport layout, selection visuals, scrolling, and comment insertion. |
| Viewport content layer | TextLayoutFragmentLayer sublayers |
Retains visible rendering layers | The view clears/repopulates them around each viewport layout pass. |
TextLayoutFragmentLayer |
One layout-fragment reference and drawing geometry | Retains the fragment used for drawing | It updates layer bounds/position and delegates glyph drawing to the fragment. |
Composition denotes source-visible construction and retained value state. Aggregation denotes a stored or injected collaborator whose exclusive lifetime the source does not prove.
Class and protocol design
iOS/TextDocumentView.swift:251 — comment-specific layout fragment selection
func textLayoutManager(_ textLayoutManager: NSTextLayoutManager,
textLayoutFragmentFor location: NSTextLocation,
in textElement: NSTextElement) -> NSTextLayoutFragment {
let index = textLayoutManager.offset(
from: textLayoutManager.documentRange.location, to: location)
let depth = textContentStorage!.textStorage!
.attribute(.commentDepth, at: index, effectiveRange: nil) as! NSNumber?
if let depth {
let fragment = BubbleLayoutFragment(
textElement: textElement, range: textElement.elementRange)
fragment.commentDepth = depth.uintValue
return fragment
}
return NSTextLayoutFragment(textElement: textElement,
range: textElement.elementRange)
}| Type | Responsibility | Depends on or conforms to |
|---|---|---|
Platform TextDocumentViewController |
Builds TextKit graph and supplies comment display/presentation policy | Content-storage manager delegates plus UIKit/AppKit controller roles |
Platform TextDocumentView |
Viewport rendering, interaction, selection, scrolling, comment insertion | Viewport/layout-manager delegates and platform input callbacks |
BubbleLayoutFragment |
Adds indentation, margins, bubble geometry, and background drawing | NSTextLayoutFragment subclass |
TextLayoutFragmentLayer |
Adapts a TextKit layout fragment to a CALayer rendering surface |
CALayer subclass |
Reaction / comment attribute |
Shared comment metadata and reaction presentation values | Shared enum and NSAttributedString.Key extension |
All protocol conformances are TextKit/UIKit/AppKit framework contracts. The sample defines no local protocol abstraction; extensibility comes from framework delegates and subclassing NSTextLayoutFragment/CALayer.
Access control
| Symbol | Access | Verified effect | Design reason |
|---|---|---|---|
| Controller content storage, layout manager, current fragment, comment flag | private |
Only the controller’s lexical implementation can mutate the TextKit session | Prevents popover or view code from bypassing controller coordination. |
| View content/selection layers, map, padding, rendering helpers | private |
Confined to each platform view implementation | Protects viewport and layer-cache invariants. |
| Bubble geometry/color/path helpers | private |
Only BubbleLayoutFragment drawing can use them |
Keeps drawing calculations behind fragment overrides. |
NSAttributedString.Key.commentDepth |
public static |
Exported with the Foundation key extension | The shared semantic attribute can be referenced across targets/modules that include the file. |
| Fragment/view/controller types | implicit internal |
Visible within their app target/module | They are sample implementation types rather than public framework API. |
Reference code
Shared/CommentUtils.swift:10 — shared public attributed-string key
extension NSAttributedString.Key {
public static var commentDepth: NSAttributedString.Key {
return NSAttributedString.Key("TK2DemoCommentDepth")
}
}
enum Reaction: Int, CaseIterable {
// Reaction metadata remains module-internal.
}The attribute key is the one explicit exported symbol; enclosing sample classes remain internal and implementation state is private. No open or fileprivate declaration is used in the shared rendering path.
Logic ownership and placement
| Logic | Owning type or file | Why it lives there |
|---|---|---|
| TextKit graph construction and source loading | Platform TextDocumentViewController |
It owns the content/layout objects and platform presentation lifecycle. |
| Viewport layer creation, reuse, geometry, selection | Platform TextDocumentView |
These decisions depend on scrolling/input coordinates and visible layout fragments. |
| Comment fragment choice | TextDocumentView layout-manager delegate |
It reads the comment-depth attribute at the requested text location. |
| Bubble drawing geometry | Shared BubbleLayoutFragment |
The subclass owns rendering-surface bounds and drawing overrides. |
| Fragment-to-layer rendering | Shared TextLayoutFragmentLayer |
It translates TextKit fragment geometry into Core Animation state. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Explicit TextKit object graph | iOS/TextDocumentViewController.swift:29 |
Makes storage, layout manager, and container ownership visible and configurable. |
| Viewport rendering adapter | iOS/TextDocumentView.swift:61 |
Creates/reuses one layer per visible layout fragment during viewport passes. |
| Layout fragment specialization | Shared/BubbleLayoutFragment.swift:15 |
Adds comment layout/drawing without branching inside the generic layer renderer. |
| Parallel platform shells | macOS/TextDocumentView.swift:17 and iOS/TextDocumentView.swift:17 |
Shares rendering primitives while keeping platform scrolling and input idiomatic. |
Naming conventions
- TextKit roles retain framework vocabulary (
TextContentStorage,TextLayoutManager,TextLayoutFragment). - Custom types add one responsibility (
BubbleLayoutFragment,TextLayoutFragmentLayer,TextDocumentView). - Private commands describe mechanics (
findOrCreateLayer,updateSelectionHighlights,adjustViewportOffset). - Platform folders intentionally reuse primary type names because each target compiles only its own shell.
Architecture takeaways
- Construct the TextKit storage/layout/container graph at the controller boundary and pass collaborators to the rendering view.
- Use the viewport delegate to materialize only visible fragment layers and a fragment subclass for specialized layout/drawing.
- Keep platform interaction shells separate while sharing fragment and layer primitives.
- Reserve public access for a true shared semantic key; keep rendering state private.
Source map
| Source file | Architectural role |
|---|---|
iOS/TextDocumentViewController.swift |
iOS TextKit graph, comment visibility, and popover presentation |
iOS/TextDocumentView.swift |
iOS viewport rendering, selection, scrolling, and comment interaction |
macOS/TextDocumentViewController.swift |
macOS TextKit graph and document-controller policy |
macOS/TextDocumentView.swift |
macOS viewport rendering, selection, scrolling, and mouse interaction |
Shared/BubbleLayoutFragment.swift |
Comment layout and bubble drawing |
Shared/TextLayoutFragmentLayer.swift |
Fragment-to-Core-Animation adapter |
Shared/CommentUtils.swift |
Shared comment attribute and reaction metadata |