TextKit 2 Reference
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.
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.
Family: TextKit Runtime And Layout
Use this skill when you already know the editor is on TextKit 2 and need exact APIs, object roles, or migration details.
When to Use
Section titled “When to Use”- You are working with
NSTextLayoutManager,NSTextContentManager, or fragments. - You need viewport-layout or migration details.
- You are writing TextKit 2 code directly rather than choosing between stacks.
Quick Decision
Section titled “Quick Decision”- Need to choose between TextKit 1 and 2 ->
/skill apple-text-layout-manager-selection - Already committed to TextKit 2 and need exact APIs -> stay here
- Need fragment/rendering behavior specifically ->
/skill apple-text-viewport-rendering
Core Guidance
Section titled “Core Guidance”Complete reference for TextKit 2 (iOS 15+ / macOS 12+). Replaces glyph-based TextKit 1 with element-based layout optimized for correctness, safety, and performance.
Keep this file for the object model, editing rules, and layout-manager behavior. For fragment internals, object-based range mechanics, and the TextKit 1 to 2 mapping table, use fragments-and-migration.md.
Architecture
Section titled “Architecture”NSTextContentManager NSTextLayoutManager NSTextContainer(content model) → (layout controller) → (geometry) │ │ │NSTextContentStorage NSTextLayoutFragment UITextView(wraps NSTextStorage) NSTextLineFragment NSTextView │ │NSTextElement NSTextViewportLayoutControllerNSTextParagraph (viewport management)Design Principles
Section titled “Design Principles”- Correctness — No glyph APIs. International text (Arabic, Devanagari, CJK) handled correctly without character-glyph mapping assumptions.
- Safety — Immutable value semantics for elements and fragments. Thread-safe reads.
- Performance — Always non-contiguous. Only viewport text is laid out. O(viewport) not O(document).
NSTextContentManager (Abstract)
Section titled “NSTextContentManager (Abstract)”Base class for content management. Manages document content as a tree of NSTextElement objects.
Key Properties
Section titled “Key Properties”var textLayoutManagers: [NSTextLayoutManager] { get }var primaryTextLayoutManager: NSTextLayoutManager? { get set }var automaticallySynchronizesTextLayoutManagers: Bool // default: truevar automaticallySynchronizesToBackingStore: Bool // default: trueEditing Transaction
Section titled “Editing Transaction”All text storage modifications must be wrapped:
textContentManager.performEditingTransaction { // Modify the backing store (NSTextStorage) here textStorage.replaceCharacters(in: range, with: newText)}// Layout invalidation happens automatically after the transactionWithout the transaction wrapper: Element regeneration and layout invalidation may not occur correctly.
Element Enumeration
Section titled “Element Enumeration”textContentManager.enumerateTextElements(from: location, options: []) { element in if let paragraph = element as? NSTextParagraph { print(paragraph.attributedString) } return true // continue enumeration}Delegate
Section titled “Delegate”// Filter elements from layout (e.g., hide comments in a code editor)func textContentManager(_ manager: NSTextContentManager, shouldEnumerate textElement: NSTextElement, options: NSTextContentManager.EnumerationOptions) -> BoolNSTextContentStorage (Concrete)
Section titled “NSTextContentStorage (Concrete)”Default NSTextContentManager subclass. Wraps NSTextStorage and automatically divides content into NSTextParagraph elements.
Relationship to NSTextStorage
Section titled “Relationship to NSTextStorage”let textContentStorage = NSTextContentStorage()textContentStorage.textStorage = myTextStorage // Set backing store
// Access text storage from content storagelet storage = textContentStorage.textStorageNSTextContentStorage observes NSTextStorage edits and regenerates paragraph elements automatically.
NSTextContentStorage vs NSTextStorage
Section titled “NSTextContentStorage vs NSTextStorage”| Aspect | NSTextStorage | NSTextContentStorage |
|---|---|---|
| Role | Backing store (attributed string) | Content manager wrapping backing store |
| Addressing | NSRange (integer-based) | NSTextRange / NSTextLocation (object-based) |
| Output | Raw attributed string | NSTextElement tree (paragraphs) |
| Editing | Direct mutations | performEditingTransaction wrapper |
| Notifications | processEditing() | Element change tracking |
| When to subclass | Custom backing store format | Custom content model (not attributed string based) |
Decision: Use NSTextContentStorage (default) unless you need a fundamentally different backing store (e.g., database-backed, DOM-based, piece table). In that case, subclass NSTextContentManager directly.
Delegate
Section titled “Delegate”// Create custom paragraph elements with modified display attributes// WITHOUT changing the underlying text storagefunc textContentStorage(_ storage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { // Return nil for default behavior // Return custom NSTextParagraph to override display let originalText = storage.textStorage!.attributedSubstring(from: range) let modified = NSMutableAttributedString(attributedString: originalText) modified.addAttribute(.foregroundColor, value: UIColor.gray, range: NSRange(location: 0, length: modified.length)) return NSTextParagraph(attributedString: modified)}Range Conversion
Section titled “Range Conversion”// NSRange → NSTextRangelet textRange = textContentStorage.textRange(for: nsRange)
// NSTextRange → NSRangelet nsRange = textContentStorage.offset(from: textContentStorage.documentRange.location, to: textRange.location)NSTextElement
Section titled “NSTextElement”Abstract base class for document building blocks. Immutable (value semantics).
Properties
Section titled “Properties”var elementRange: NSTextRange? { get set } // Range within documentvar textContentManager: NSTextContentManager? { get }var childElements: [NSTextElement] { get } // For nested structuresvar parentElement: NSTextElement? { get }var isRepresentedElement: Bool { get }NSTextParagraph
Section titled “NSTextParagraph”Default element type. One per paragraph of text.
let paragraph: NSTextParagraphparagraph.attributedString // The paragraph's attributed contentparagraph.paragraphContentRange // Range excluding paragraph separatorparagraph.paragraphSeparators // The paragraph separator charactersNSTextLayoutManager
Section titled “NSTextLayoutManager”Replaces NSLayoutManager. No glyph APIs. Operates on elements and fragments.
Key Properties
Section titled “Key Properties”var textContentManager: NSTextContentManager? { get }var textContainer: NSTextContainer? { get set }var textViewportLayoutController: NSTextViewportLayoutController { get }var textSelectionNavigation: NSTextSelectionNavigation { get }var textSelections: [NSTextSelection] { get set }var usageBoundsForTextContainer: CGRect { get }var documentRange: NSTextRange { get }Layout Fragment Enumeration
Section titled “Layout Fragment Enumeration”// Enumerate visible layout fragmentstextLayoutManager.enumerateTextLayoutFragments( from: textLayoutManager.documentRange.location, options: [.ensuresLayout, .ensuresExtraLineFragment]) { fragment in print("Frame: \(fragment.layoutFragmentFrame)") for lineFragment in fragment.textLineFragments { print(" Line: \(lineFragment.typographicBounds)") } return true // continue}Options:
.ensuresLayout— Forces layout computation (expensive for large ranges).ensuresExtraLineFragment— Includes empty trailing line fragment.estimatesSize— Use estimated sizes (faster, less accurate).reverse— Enumerate backwards
Rendering Attributes
Section titled “Rendering Attributes”Replace TextKit 1’s temporary attributes. Overlay visual styling without modifying text storage:
// Set rendering attributes (replaces any existing)textLayoutManager.setRenderingAttributes( [.foregroundColor: UIColor.red], forTextRange: range)
// Add rendering attributes (merges)textLayoutManager.addRenderingAttribute(.backgroundColor, value: UIColor.yellow, forTextRange: range)
// Remove rendering attributestextLayoutManager.removeRenderingAttribute(.backgroundColor, forTextRange: range)
// Enumerate rendering attributestextLayoutManager.enumerateRenderingAttributes( from: location, reverse: false) { manager, attributes, range in return true}Key difference from text storage attributes: Rendering attributes don’t persist, don’t modify the model, and don’t trigger element regeneration.
Invalidating Layout
Section titled “Invalidating Layout”// Invalidate specific rangetextLayoutManager.invalidateLayout(for: range)
// TextKit 2 re-lays out affected fragments on next viewport updateDelegate
Section titled “Delegate”// Custom layout fragments (e.g., chat bubble backgrounds)func textLayoutManager(_ manager: NSTextLayoutManager, textLayoutFragmentFor location: NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment { return BubbleLayoutFragment(textElement: textElement, range: textElement.elementRange)}Common Pitfalls
Section titled “Common Pitfalls”- Using
ensuresLayoutfor the full document — O(document_size). Only ensure layout for visible ranges. - NSTextLineFragment.characterRange is local — It’s relative to the line’s attributed string, NOT the document. Convert through the parent element.
renderingSurfaceBoundsdiffers fromlayoutFragmentFrame— Drawing can extend beyond the layout frame (diacritics, large descenders). OverriderenderingSurfaceBoundsin custom fragments.- Forgetting
performEditingTransaction— Direct NSTextStorage edits may not trigger proper element regeneration. - Assuming layout exists outside viewport — TextKit 2 may only have estimated layout for off-screen content. Use
.estimatesSizeoption when precision isn’t needed.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-textkit2-ref 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-manager-selection: Use when the main task is choosing between TextKit 1 and TextKit 2, especially NSLayoutManager versus NSTextLayoutManager for performance, migration risk, large documents, or feature fit. Reach for this when the stack choice is still open, not when the user already needs API-level mechanics.apple-text-storage: 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.apple-text-viewport-rendering: Use when the user needs to understand how Apple text actually renders on screen: viewport layout, line-fragment geometry, rendering attributes, font substitution, fixAttributes, scroll-driven layout, or TextKit versus Core Text drawing differences. Reach for this when the issue is rendering mechanics, not generic layout invalidation.
Sidecar Files
Section titled “Sidecar Files”skills/apple-text-textkit2-ref/fragments-and-migration.md
Full SKILL.md source
---name: apple-text-textkit2-refdescription: > 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.license: MIT---
# TextKit 2 Reference
Use this skill when you already know the editor is on TextKit 2 and need exact APIs, object roles, or migration details.
## When to Use
- You are working with `NSTextLayoutManager`, `NSTextContentManager`, or fragments.- You need viewport-layout or migration details.- You are writing TextKit 2 code directly rather than choosing between stacks.
## Quick Decision
- Need to choose between TextKit 1 and 2 -> `/skill apple-text-layout-manager-selection`- Already committed to TextKit 2 and need exact APIs -> stay here- Need fragment/rendering behavior specifically -> `/skill apple-text-viewport-rendering`
## Core Guidance
Complete reference for TextKit 2 (iOS 15+ / macOS 12+). Replaces glyph-based TextKit 1 with element-based layout optimized for correctness, safety, and performance.
Keep this file for the object model, editing rules, and layout-manager behavior. For fragment internals, object-based range mechanics, and the TextKit 1 to 2 mapping table, use [fragments-and-migration.md](fragments-and-migration.md).
## Architecture
```NSTextContentManager NSTextLayoutManager NSTextContainer(content model) → (layout controller) → (geometry) │ │ │NSTextContentStorage NSTextLayoutFragment UITextView(wraps NSTextStorage) NSTextLineFragment NSTextView │ │NSTextElement NSTextViewportLayoutControllerNSTextParagraph (viewport management)```
### Design Principles
1. **Correctness** — No glyph APIs. International text (Arabic, Devanagari, CJK) handled correctly without character-glyph mapping assumptions.2. **Safety** — Immutable value semantics for elements and fragments. Thread-safe reads.3. **Performance** — Always non-contiguous. Only viewport text is laid out. O(viewport) not O(document).
## NSTextContentManager (Abstract)
Base class for content management. Manages document content as a tree of `NSTextElement` objects.
### Key Properties
```swiftvar textLayoutManagers: [NSTextLayoutManager] { get }var primaryTextLayoutManager: NSTextLayoutManager? { get set }var automaticallySynchronizesTextLayoutManagers: Bool // default: truevar automaticallySynchronizesToBackingStore: Bool // default: true```
### Editing Transaction
All text storage modifications must be wrapped:
```swifttextContentManager.performEditingTransaction { // Modify the backing store (NSTextStorage) here textStorage.replaceCharacters(in: range, with: newText)}// Layout invalidation happens automatically after the transaction```
**Without the transaction wrapper:** Element regeneration and layout invalidation may not occur correctly.
### Element Enumeration
```swifttextContentManager.enumerateTextElements(from: location, options: []) { element in if let paragraph = element as? NSTextParagraph { print(paragraph.attributedString) } return true // continue enumeration}```
### Delegate
```swift// Filter elements from layout (e.g., hide comments in a code editor)func textContentManager(_ manager: NSTextContentManager, shouldEnumerate textElement: NSTextElement, options: NSTextContentManager.EnumerationOptions) -> Bool```
## NSTextContentStorage (Concrete)
Default `NSTextContentManager` subclass. Wraps `NSTextStorage` and automatically divides content into `NSTextParagraph` elements.
### Relationship to NSTextStorage
```swiftlet textContentStorage = NSTextContentStorage()textContentStorage.textStorage = myTextStorage // Set backing store
// Access text storage from content storagelet storage = textContentStorage.textStorage```
**NSTextContentStorage observes NSTextStorage edits** and regenerates paragraph elements automatically.
### NSTextContentStorage vs NSTextStorage
| Aspect | NSTextStorage | NSTextContentStorage ||--------|--------------|---------------------|| **Role** | Backing store (attributed string) | Content manager wrapping backing store || **Addressing** | NSRange (integer-based) | NSTextRange / NSTextLocation (object-based) || **Output** | Raw attributed string | NSTextElement tree (paragraphs) || **Editing** | Direct mutations | `performEditingTransaction` wrapper || **Notifications** | `processEditing()` | Element change tracking || **When to subclass** | Custom backing store format | Custom content model (not attributed string based) |
**Decision:** Use NSTextContentStorage (default) unless you need a fundamentally different backing store (e.g., database-backed, DOM-based, piece table). In that case, subclass NSTextContentManager directly.
### Delegate
```swift// Create custom paragraph elements with modified display attributes// WITHOUT changing the underlying text storagefunc textContentStorage(_ storage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { // Return nil for default behavior // Return custom NSTextParagraph to override display let originalText = storage.textStorage!.attributedSubstring(from: range) let modified = NSMutableAttributedString(attributedString: originalText) modified.addAttribute(.foregroundColor, value: UIColor.gray, range: NSRange(location: 0, length: modified.length)) return NSTextParagraph(attributedString: modified)}```
### Range Conversion
```swift// NSRange → NSTextRangelet textRange = textContentStorage.textRange(for: nsRange)
// NSTextRange → NSRangelet nsRange = textContentStorage.offset(from: textContentStorage.documentRange.location, to: textRange.location)```
## NSTextElement
Abstract base class for document building blocks. Immutable (value semantics).
### Properties
```swiftvar elementRange: NSTextRange? { get set } // Range within documentvar textContentManager: NSTextContentManager? { get }var childElements: [NSTextElement] { get } // For nested structuresvar parentElement: NSTextElement? { get }var isRepresentedElement: Bool { get }```
## NSTextParagraph
Default element type. One per paragraph of text.
```swiftlet paragraph: NSTextParagraphparagraph.attributedString // The paragraph's attributed contentparagraph.paragraphContentRange // Range excluding paragraph separatorparagraph.paragraphSeparators // The paragraph separator characters```
## NSTextLayoutManager
Replaces NSLayoutManager. **No glyph APIs.** Operates on elements and fragments.
### Key Properties
```swiftvar textContentManager: NSTextContentManager? { get }var textContainer: NSTextContainer? { get set }var textViewportLayoutController: NSTextViewportLayoutController { get }var textSelectionNavigation: NSTextSelectionNavigation { get }var textSelections: [NSTextSelection] { get set }var usageBoundsForTextContainer: CGRect { get }var documentRange: NSTextRange { get }```
### Layout Fragment Enumeration
```swift// Enumerate visible layout fragmentstextLayoutManager.enumerateTextLayoutFragments( from: textLayoutManager.documentRange.location, options: [.ensuresLayout, .ensuresExtraLineFragment]) { fragment in print("Frame: \(fragment.layoutFragmentFrame)") for lineFragment in fragment.textLineFragments { print(" Line: \(lineFragment.typographicBounds)") } return true // continue}```
**Options:**- `.ensuresLayout` — Forces layout computation (expensive for large ranges)- `.ensuresExtraLineFragment` — Includes empty trailing line fragment- `.estimatesSize` — Use estimated sizes (faster, less accurate)- `.reverse` — Enumerate backwards
### Rendering Attributes
Replace TextKit 1's temporary attributes. Overlay visual styling without modifying text storage:
```swift// Set rendering attributes (replaces any existing)textLayoutManager.setRenderingAttributes( [.foregroundColor: UIColor.red], forTextRange: range)
// Add rendering attributes (merges)textLayoutManager.addRenderingAttribute(.backgroundColor, value: UIColor.yellow, forTextRange: range)
// Remove rendering attributestextLayoutManager.removeRenderingAttribute(.backgroundColor, forTextRange: range)
// Enumerate rendering attributestextLayoutManager.enumerateRenderingAttributes( from: location, reverse: false) { manager, attributes, range in return true}```
**Key difference from text storage attributes:** Rendering attributes don't persist, don't modify the model, and don't trigger element regeneration.
### Invalidating Layout
```swift// Invalidate specific rangetextLayoutManager.invalidateLayout(for: range)
// TextKit 2 re-lays out affected fragments on next viewport update```
### Delegate
```swift// Custom layout fragments (e.g., chat bubble backgrounds)func textLayoutManager(_ manager: NSTextLayoutManager, textLayoutFragmentFor location: NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment { return BubbleLayoutFragment(textElement: textElement, range: textElement.elementRange)}```
## Common Pitfalls
1. **Using `ensuresLayout` for the full document** — O(document_size). Only ensure layout for visible ranges.2. **NSTextLineFragment.characterRange is local** — It's relative to the line's attributed string, NOT the document. Convert through the parent element.3. **`renderingSurfaceBounds` differs from `layoutFragmentFrame`** — Drawing can extend beyond the layout frame (diacritics, large descenders). Override `renderingSurfaceBounds` in custom fragments.4. **Forgetting `performEditingTransaction`** — Direct NSTextStorage edits may not trigger proper element regeneration.5. **Assuming layout exists outside viewport** — TextKit 2 may only have estimated layout for off-screen content. Use `.estimatesSize` option when precision isn't needed.
## Related Skills
- For fragment APIs, viewport controller hooks, range conversion, and migration tables, see [fragments-and-migration.md](fragments-and-migration.md).- Use `/skill apple-text-layout-manager-selection` for migration and stack choice.- Use `/skill apple-text-viewport-rendering` for fragment and rendering-pipeline behavior.- Use `/skill apple-text-storage` for backing-store and editing-transaction background.