Text Storage Architecture
Use when the user is working on NSTextStorage, NSTextContentStorage, or NSTextContentManager and needs the text-model architecture, subclassing rules, delegate hooks, or processEditing lifecycle. Reach for this when the storage layer is the focus, not general TextKit choice or symptom-first debugging.
Use when the user is working on NSTextStorage, NSTextContentStorage, or NSTextContentManager and needs the text-model architecture, subclassing rules, delegate hooks, or processEditing lifecycle. Reach for this when the storage layer is the focus, not general TextKit choice or symptom-first debugging.
Family: Text Model And Foundation Utilities
Use this skill when the main question is how text content is stored, mutated, and synchronized with layout.
Keep this file for the storage architecture, editing lifecycle, and common pitfalls. For custom backing stores (piece table, rope, CRDT), thread-safety patterns, and performance profiling, use advanced-patterns.md.
When to Use
Section titled “When to Use”- You are editing or subclassing
NSTextStorage. - You need to understand
NSTextContentStorageorNSTextContentManager. - You are debugging storage-layer behavior beneath layout or rendering symptoms.
Quick Decision
Section titled “Quick Decision”- Need invalidation behavior after edits ->
/skill apple-text-layout-invalidation - Need storage architecture and editing lifecycle -> stay here
- Need TextKit 1 or 2 API detail after choosing a stack -> jump to the matching
*-refskill
Core Guidance
Section titled “Core Guidance”Architecture Overview
Section titled “Architecture Overview”TextKit 1 Storage
Section titled “TextKit 1 Storage”NSTextStorage (IS-A NSMutableAttributedString) │ ├── stores characters + attributes ├── processEditing() lifecycle └── notifies → NSLayoutManager(s)TextKit 2 Storage
Section titled “TextKit 2 Storage”NSTextContentManager (abstract) │ └── NSTextContentStorage (concrete) │ ├── wraps → NSTextStorage ├── generates → NSTextParagraph elements └── notifies → NSTextLayoutManager(s)The Key Difference
Section titled “The Key Difference”- TextKit 1: NSTextStorage is the ONLY model layer. Layout managers read directly from it.
- TextKit 2: NSTextContentStorage adds an element layer on top of NSTextStorage. Layout managers work with elements (NSTextParagraph), not raw attributed strings.
NSTextStorage
Section titled “NSTextStorage”What It Is
Section titled “What It Is”NSTextStorage is a subclass of NSMutableAttributedString. It IS an attributed string with additional change-tracking and notification machinery.
class NSTextStorage: NSMutableAttributedString { var layoutManagers: [NSLayoutManager] { get } var editedMask: EditActions { get } var editedRange: NSRange { get } var changeInLength: Int { get }
func addLayoutManager(_ aLayoutManager: NSLayoutManager) func removeLayoutManager(_ aLayoutManager: NSLayoutManager)
func edited(_ editedMask: EditActions, range editedRange: NSRange, changeInLength delta: Int) func processEditing()
var delegate: NSTextStorageDelegate?}Editing Lifecycle (Complete)
Section titled “Editing Lifecycle (Complete)” ┌─────────────────────────────┐ │ External mutation │ │ (replaceCharacters, etc.) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ edited(_:range:delta:) │ │ Accumulates edit tracking: │ │ - editedMask |= mask │ │ - editedRange = union(old, │ │ new, adjusted for delta) │ │ - changeInLength += delta │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ endEditing() called │ │ (or auto if no batch) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ processEditing() │ └──────────────┬──────────────┘ │ ┌────────────────────┼────────────────────┐ │ │ │ ┌──────────▼──────────┐ ┌─────▼─────┐ ┌──────────▼──────────┐ │ willProcessEditing │ │ fixAttrs │ │ didProcessEditing │ │ delegate callback │ │ (system) │ │ delegate callback │ │ │ │ │ │ │ │ Can modify: │ │ Font sub, │ │ Can modify: │ │ - Characters ✅ │ │ paragraph │ │ - Attributes ✅ │ │ - Attributes ✅ │ │ fixing │ │ - Characters ❌ │ └──────────────────────┘ └───────────┘ └─────────┬───────────┘ │ ┌────────────▼────────────┐ │ Notify layout managers │ │ processEditing(for: │ │ edited:range: │ │ changeInLength: │ │ invalidatedRange:) │ └─────────────────────────┘Batching Edits
Section titled “Batching Edits”textStorage.beginEditing()
// Multiple mutations — each calls edited() internallytextStorage.replaceCharacters(in: range1, with: "new text")textStorage.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 16), range: range2)textStorage.deleteCharacters(in: range3)
textStorage.endEditing()// processEditing() called ONCE with accumulated editsWithout batching: Each mutation triggers processEditing() separately = multiple layout invalidation passes.
Subclassing NSTextStorage
Section titled “Subclassing NSTextStorage”Required when you want a custom backing store (e.g., rope data structure, gap buffer, piece table).
Four required primitives:
class RopeTextStorage: NSTextStorage { private var rope = Rope() // Your custom backing store
// 1. Read string content override var string: String { rope.string }
// 2. Read attributes at location override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] { rope.attributes(at: location, effectiveRange: range) }
// 3. Replace characters (MUST call edited()) override func replaceCharacters(in range: NSRange, with str: String) { beginEditing() rope.replaceCharacters(in: range, with: str) edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length) endEditing() }
// 4. Set attributes (MUST call edited()) override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) { beginEditing() rope.setAttributes(attrs, range: range) edited(.editedAttributes, range: range, changeInLength: 0) endEditing() }}Critical rules for subclasses:
replaceCharactersandsetAttributesMUST calledited(_:range:changeInLength:)with correct maskedited()with.editedCharactersmust include accuratechangeInLength- The
stringproperty must always reflect current content attributes(at:effectiveRange:)must handle the full range correctly
Delegate Protocol
Section titled “Delegate Protocol”protocol NSTextStorageDelegate: NSObjectProtocol { // Called BEFORE fixAttributes — can modify characters AND attributes func textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
// Called AFTER fixAttributes — can modify ONLY attributes func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)}Common use cases:
willProcessEditing: Auto-correct, text transforms, syntax detectiondidProcessEditing: Syntax highlighting (apply color attributes based on content)
NSTextContentStorage (TextKit 2)
Section titled “NSTextContentStorage (TextKit 2)”What It Is
Section titled “What It Is”Concrete subclass of NSTextContentManager that bridges NSTextStorage to the TextKit 2 element model.
class NSTextContentStorage: NSTextContentManager { var textStorage: NSTextStorage? { get set } var attributedString: NSAttributedString? { get set }
func textRange(for range: NSRange) -> NSTextRange? func offset(from: NSTextLocation, to: NSTextLocation) -> Int
var delegate: NSTextContentStorageDelegate?}How It Works
Section titled “How It Works”- NSTextContentStorage observes NSTextStorage edit notifications
- When text storage changes, it regenerates affected
NSTextParagraphelements - Paragraph boundaries are determined by paragraph separators (
\n,\r\n,\r,\u{2029}) - Each paragraph becomes one
NSTextParagraphwith the paragraph’s attributed text
Editing Pattern
Section titled “Editing Pattern”// ✅ CORRECT: Wrap edits in transactiontextContentStorage.performEditingTransaction { textStorage.replaceCharacters(in: range, with: newText)}
// ❌ WRONG: Direct edit without transactiontextStorage.replaceCharacters(in: range, with: newText)// May not trigger proper element regenerationDelegate
Section titled “Delegate”protocol NSTextContentStorageDelegate: NSTextContentManagerDelegate { // Create custom paragraph elements with display-only modifications func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph?}Use case: Return modified paragraph for display without changing the underlying storage (e.g., show line numbers, fold code, render Markdown preview).
NSTextContentManager (Abstract)
Section titled “NSTextContentManager (Abstract)”When to Subclass Directly
Section titled “When to Subclass Directly”Subclass NSTextContentManager (instead of using NSTextContentStorage) when your backing store is NOT an attributed string:
- Database-backed document model
- HTML DOM
- AST (abstract syntax tree)
- Collaborative editing CRDT
Required Overrides
Section titled “Required Overrides”class DOMContentManager: NSTextContentManager { override var documentRange: NSTextRange { ... }
override func enumerateTextElements( from textLocation: NSTextLocation?, options: NSTextContentManager.EnumerationOptions, using block: (NSTextElement) -> Bool ) { ... }
override func replaceContents( in range: NSTextRange, with textElements: [NSTextElement]? ) { ... }
override func location( _ location: NSTextLocation, offsetBy offset: Int ) -> NSTextLocation? { ... }
override func offset( from: NSTextLocation, to: NSTextLocation ) -> Int { ... }}Storage Layer Relationships
Section titled “Storage Layer Relationships”┌─────────────────────────────────────────────────────────────┐│ TextKit 1 Only ││ ││ NSTextStorage ──────────────────────→ NSLayoutManager(s) ││ (attributed string = backing store) │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ TextKit 2 ││ ││ NSTextStorage ──→ NSTextContentStorage ──→ NSTextLayout- ││ (backing store) (element generator) Manager(s) ││ │ ││ NSTextParagraph(s) ││ (element tree) │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ Custom TextKit 2 ││ ││ Custom Store ──→ NSTextContentManager ──→ NSTextLayout- ││ (any format) (custom subclass) Manager(s) ││ │ ││ Custom NSTextElement(s) │└─────────────────────────────────────────────────────────────┘Common Pitfalls
Section titled “Common Pitfalls”- Not calling
edited()in NSTextStorage subclass — Layout managers never learn about changes. The most common subclassing bug. - Wrong
changeInLengthvalue — Causes range calculation errors, crashes, or corrupted layout. - Modifying characters in
didProcessEditing— Characters are already committed. Attribute-only modifications allowed here. - Direct NSTextStorage edit without
performEditingTransaction(TextKit 2) — Element tree may not update correctly. - Accessing
textStorage.stringduring processEditing — The string is valid, but indices from before the edit are invalid if characters changed. - Not batching edits —
beginEditing()/endEditing()exists for a reason. Use it for multi-mutation operations.
Going Deeper
Section titled “Going Deeper”Read advanced-patterns.md in this skill directory for:
- Custom backing stores (piece table, rope, CRDT) with subclassing examples
- Thread-safety patterns for background processing with version guards
- NSTextContentManager subclassing for non-attributed-string document models
- Performance measurement with
os_signpostinstrumentation
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-storage reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.
Related
Section titled “Related”apple-text-layout-invalidation: Use when text layout stays stale, metrics do not refresh, or the user needs the exact invalidation model in TextKit 1 or TextKit 2. Reach for this when the problem is layout recalculation and ensureLayout-style mechanics, not broader rendering or storage architecture.apple-text-textkit1-ref: Use when the user is already on TextKit 1 and needs exact NSLayoutManager, NSTextStorage, or NSTextContainer APIs, glyph and layout lifecycle details, temporary attributes, exclusion paths, or multi-container behavior. Reach for this when the stack choice is already made and the task is reference-level TextKit 1 mechanics, not stack selection or symptom-first debugging.apple-text-textkit2-ref: Use when the user is already on TextKit 2 and needs exact NSTextLayoutManager, NSTextContentManager, NSTextContentStorage, viewport layout, fragment, rendering-attribute, or migration details. Reach for this when the stack choice is already made and the task is reference-level TextKit 2 mechanics, not stack selection or generic text-system debugging.
Sidecar Files
Section titled “Sidecar Files”skills/apple-text-storage/advanced-patterns.md
Full SKILL.md source
---name: apple-text-storagedescription: Use when the user is working on NSTextStorage, NSTextContentStorage, or NSTextContentManager and needs the text-model architecture, subclassing rules, delegate hooks, or processEditing lifecycle. Reach for this when the storage layer is the focus, not general TextKit choice or symptom-first debugging.license: MIT---
# Text Storage Architecture
Use this skill when the main question is how text content is stored, mutated, and synchronized with layout.
Keep this file for the storage architecture, editing lifecycle, and common pitfalls. For custom backing stores (piece table, rope, CRDT), thread-safety patterns, and performance profiling, use [advanced-patterns.md](advanced-patterns.md).
## When to Use
- You are editing or subclassing `NSTextStorage`.- You need to understand `NSTextContentStorage` or `NSTextContentManager`.- You are debugging storage-layer behavior beneath layout or rendering symptoms.
## Quick Decision
- Need invalidation behavior after edits -> `/skill apple-text-layout-invalidation`- Need storage architecture and editing lifecycle -> stay here- Need TextKit 1 or 2 API detail after choosing a stack -> jump to the matching `*-ref` skill
## Core Guidance
## Architecture Overview
### TextKit 1 Storage
```NSTextStorage (IS-A NSMutableAttributedString) │ ├── stores characters + attributes ├── processEditing() lifecycle └── notifies → NSLayoutManager(s)```
### TextKit 2 Storage
```NSTextContentManager (abstract) │ └── NSTextContentStorage (concrete) │ ├── wraps → NSTextStorage ├── generates → NSTextParagraph elements └── notifies → NSTextLayoutManager(s)```
### The Key Difference
- **TextKit 1:** NSTextStorage is the ONLY model layer. Layout managers read directly from it.- **TextKit 2:** NSTextContentStorage adds an **element layer** on top of NSTextStorage. Layout managers work with elements (NSTextParagraph), not raw attributed strings.
## NSTextStorage
### What It Is
`NSTextStorage` is a subclass of `NSMutableAttributedString`. It IS an attributed string with additional change-tracking and notification machinery.
```swiftclass NSTextStorage: NSMutableAttributedString { var layoutManagers: [NSLayoutManager] { get } var editedMask: EditActions { get } var editedRange: NSRange { get } var changeInLength: Int { get }
func addLayoutManager(_ aLayoutManager: NSLayoutManager) func removeLayoutManager(_ aLayoutManager: NSLayoutManager)
func edited(_ editedMask: EditActions, range editedRange: NSRange, changeInLength delta: Int) func processEditing()
var delegate: NSTextStorageDelegate?}```
### Editing Lifecycle (Complete)
``` ┌─────────────────────────────┐ │ External mutation │ │ (replaceCharacters, etc.) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ edited(_:range:delta:) │ │ Accumulates edit tracking: │ │ - editedMask |= mask │ │ - editedRange = union(old, │ │ new, adjusted for delta) │ │ - changeInLength += delta │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ endEditing() called │ │ (or auto if no batch) │ └──────────────┬──────────────┘ │ ┌──────────────▼──────────────┐ │ processEditing() │ └──────────────┬──────────────┘ │ ┌────────────────────┼────────────────────┐ │ │ │ ┌──────────▼──────────┐ ┌─────▼─────┐ ┌──────────▼──────────┐ │ willProcessEditing │ │ fixAttrs │ │ didProcessEditing │ │ delegate callback │ │ (system) │ │ delegate callback │ │ │ │ │ │ │ │ Can modify: │ │ Font sub, │ │ Can modify: │ │ - Characters ✅ │ │ paragraph │ │ - Attributes ✅ │ │ - Attributes ✅ │ │ fixing │ │ - Characters ❌ │ └──────────────────────┘ └───────────┘ └─────────┬───────────┘ │ ┌────────────▼────────────┐ │ Notify layout managers │ │ processEditing(for: │ │ edited:range: │ │ changeInLength: │ │ invalidatedRange:) │ └─────────────────────────┘```
### Batching Edits
```swifttextStorage.beginEditing()
// Multiple mutations — each calls edited() internallytextStorage.replaceCharacters(in: range1, with: "new text")textStorage.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 16), range: range2)textStorage.deleteCharacters(in: range3)
textStorage.endEditing()// processEditing() called ONCE with accumulated edits```
**Without batching:** Each mutation triggers `processEditing()` separately = multiple layout invalidation passes.
### Subclassing NSTextStorage
Required when you want a custom backing store (e.g., rope data structure, gap buffer, piece table).
**Four required primitives:**
```swiftclass RopeTextStorage: NSTextStorage { private var rope = Rope() // Your custom backing store
// 1. Read string content override var string: String { rope.string }
// 2. Read attributes at location override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] { rope.attributes(at: location, effectiveRange: range) }
// 3. Replace characters (MUST call edited()) override func replaceCharacters(in range: NSRange, with str: String) { beginEditing() rope.replaceCharacters(in: range, with: str) edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length) endEditing() }
// 4. Set attributes (MUST call edited()) override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) { beginEditing() rope.setAttributes(attrs, range: range) edited(.editedAttributes, range: range, changeInLength: 0) endEditing() }}```
**Critical rules for subclasses:**- `replaceCharacters` and `setAttributes` MUST call `edited(_:range:changeInLength:)` with correct mask- `edited()` with `.editedCharacters` must include accurate `changeInLength`- The `string` property must always reflect current content- `attributes(at:effectiveRange:)` must handle the full range correctly
### Delegate Protocol
```swiftprotocol NSTextStorageDelegate: NSObjectProtocol { // Called BEFORE fixAttributes — can modify characters AND attributes func textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
// Called AFTER fixAttributes — can modify ONLY attributes func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)}```
**Common use cases:**- `willProcessEditing`: Auto-correct, text transforms, syntax detection- `didProcessEditing`: Syntax highlighting (apply color attributes based on content)
## NSTextContentStorage (TextKit 2)
### What It Is
Concrete subclass of `NSTextContentManager` that bridges NSTextStorage to the TextKit 2 element model.
```swiftclass NSTextContentStorage: NSTextContentManager { var textStorage: NSTextStorage? { get set } var attributedString: NSAttributedString? { get set }
func textRange(for range: NSRange) -> NSTextRange? func offset(from: NSTextLocation, to: NSTextLocation) -> Int
var delegate: NSTextContentStorageDelegate?}```
### How It Works
1. NSTextContentStorage **observes** NSTextStorage edit notifications2. When text storage changes, it **regenerates** affected `NSTextParagraph` elements3. Paragraph boundaries are determined by paragraph separators (`\n`, `\r\n`, `\r`, `\u{2029}`)4. Each paragraph becomes one `NSTextParagraph` with the paragraph's attributed text
### Editing Pattern
```swift// ✅ CORRECT: Wrap edits in transactiontextContentStorage.performEditingTransaction { textStorage.replaceCharacters(in: range, with: newText)}
// ❌ WRONG: Direct edit without transactiontextStorage.replaceCharacters(in: range, with: newText)// May not trigger proper element regeneration```
### Delegate
```swiftprotocol NSTextContentStorageDelegate: NSTextContentManagerDelegate { // Create custom paragraph elements with display-only modifications func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph?}```
**Use case:** Return modified paragraph for display without changing the underlying storage (e.g., show line numbers, fold code, render Markdown preview).
## NSTextContentManager (Abstract)
### When to Subclass Directly
Subclass `NSTextContentManager` (instead of using `NSTextContentStorage`) when your backing store is NOT an attributed string:
- Database-backed document model- HTML DOM- AST (abstract syntax tree)- Collaborative editing CRDT
### Required Overrides
```swiftclass DOMContentManager: NSTextContentManager { override var documentRange: NSTextRange { ... }
override func enumerateTextElements( from textLocation: NSTextLocation?, options: NSTextContentManager.EnumerationOptions, using block: (NSTextElement) -> Bool ) { ... }
override func replaceContents( in range: NSTextRange, with textElements: [NSTextElement]? ) { ... }
override func location( _ location: NSTextLocation, offsetBy offset: Int ) -> NSTextLocation? { ... }
override func offset( from: NSTextLocation, to: NSTextLocation ) -> Int { ... }}```
## Storage Layer Relationships
```┌─────────────────────────────────────────────────────────────┐│ TextKit 1 Only ││ ││ NSTextStorage ──────────────────────→ NSLayoutManager(s) ││ (attributed string = backing store) │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ TextKit 2 ││ ││ NSTextStorage ──→ NSTextContentStorage ──→ NSTextLayout- ││ (backing store) (element generator) Manager(s) ││ │ ││ NSTextParagraph(s) ││ (element tree) │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ Custom TextKit 2 ││ ││ Custom Store ──→ NSTextContentManager ──→ NSTextLayout- ││ (any format) (custom subclass) Manager(s) ││ │ ││ Custom NSTextElement(s) │└─────────────────────────────────────────────────────────────┘```
## Common Pitfalls
1. **Not calling `edited()` in NSTextStorage subclass** — Layout managers never learn about changes. The most common subclassing bug.2. **Wrong `changeInLength` value** — Causes range calculation errors, crashes, or corrupted layout.3. **Modifying characters in `didProcessEditing`** — Characters are already committed. Attribute-only modifications allowed here.4. **Direct NSTextStorage edit without `performEditingTransaction` (TextKit 2)** — Element tree may not update correctly.5. **Accessing `textStorage.string` during processEditing** — The string is valid, but indices from before the edit are invalid if characters changed.6. **Not batching edits** — `beginEditing()`/`endEditing()` exists for a reason. Use it for multi-mutation operations.
## Going Deeper
Read `advanced-patterns.md` in this skill directory for:
- Custom backing stores (piece table, rope, CRDT) with subclassing examples- Thread-safety patterns for background processing with version guards- NSTextContentManager subclassing for non-attributed-string document models- Performance measurement with `os_signpost` instrumentation
## Related Skills
- Use `/skill apple-text-layout-invalidation` for what re-renders or recomputes after storage edits.- Use `/skill apple-text-textkit1-ref` and `/skill apple-text-textkit2-ref` for stack-specific APIs.- Use `/skill apple-text-textkit-diag` when the symptom matters more than the storage model.