TextKit 1 Reference
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.
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.
Family: TextKit Runtime And Layout
Use this skill when you already know the editor is on TextKit 1 and need the exact APIs or lifecycle details.
When to Use
Section titled “When to Use”- You are working with
NSLayoutManager. - You need glyph-based APIs.
- You are maintaining legacy or explicitly opt-in TextKit 1 code.
Quick Decision
Section titled “Quick Decision”- Need to choose between TextKit 1 and 2 ->
/skill apple-text-layout-manager-selection - Already committed to TextKit 1 and need exact APIs -> stay here
- Debugging symptoms before you know the root cause ->
/skill apple-text-textkit-diag
Core Guidance
Section titled “Core Guidance”Complete reference for TextKit 1 covering the NSLayoutManager-based text system available since iOS 7 / macOS 10.0.
Architecture (MVC Triad)
Section titled “Architecture (MVC Triad)”NSTextStorage (Model) ←→ NSLayoutManager (Controller) ←→ NSTextContainer → UITextView/NSTextView (View) │ │ │ Attributed string Glyphs + layout Geometric region Character storage Glyph → character mapping Exclusion paths Edit notifications Line fragment rects Size constraintsOne-to-many relationships:
- One NSTextStorage → many NSLayoutManagers (same text, different layouts)
- One NSLayoutManager → many NSTextContainers (multi-page/multi-column)
- One NSTextContainer → one UITextView/NSTextView
NSTextStorage
Section titled “NSTextStorage”Subclass of NSMutableAttributedString. The canonical backing store for all TextKit text.
Required Primitives (When Subclassing)
Section titled “Required Primitives (When Subclassing)”You must subclass NSTextStorage if you want a custom backing store. Implement these four:
class CustomTextStorage: NSTextStorage { private var storage = NSMutableAttributedString()
override var string: String { storage.string }
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] { storage.attributes(at: location, effectiveRange: range) }
override func replaceCharacters(in range: NSRange, with str: String) { beginEditing() storage.replaceCharacters(in: range, with: str) edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length) endEditing() }
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) { beginEditing() storage.setAttributes(attrs, range: range) edited(.editedAttributes, range: range, changeInLength: 0) endEditing() }}Critical: Mutation methods MUST call edited(_:range:changeInLength:) with the correct mask (.editedCharacters, .editedAttributes, or both). Without this, layout managers won’t be notified.
Editing Lifecycle
Section titled “Editing Lifecycle”beginEditing()├── replaceCharacters(in:with:) → calls edited(.editedCharacters, ...)├── setAttributes(_:range:) → calls edited(.editedAttributes, ...)├── addAttribute(_:value:range:) → calls edited(.editedAttributes, ...)└── endEditing() └── processEditing() ├── delegate.textStorage(_:willProcessEditing:range:changeInLength:) │ └── Can modify BOTH characters AND attributes ├── fixAttributes(in:) — font substitution, paragraph style fixing ├── delegate.textStorage(_:didProcessEditing:range:changeInLength:) │ └── Can modify ONLY attributes (characters → crash/undefined) └── Notifies all attached layout managers └── layoutManager.processEditing(for:edited:range:changeInLength:invalidatedRange:)Batching edits: Wrap multiple mutations in beginEditing()/endEditing() to coalesce into one processEditing() call. Without batching, each mutation triggers a separate layout invalidation pass.
Edit Masks
Section titled “Edit Masks”NSTextStorage.EditActions.editedCharacters // Text content changedNSTextStorage.EditActions.editedAttributes // Attributes changed (no text change)// Combine: [.editedCharacters, .editedAttributes]Delegate Methods
Section titled “Delegate Methods”// BEFORE attribute fixing — can modify characters AND attributesfunc textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
// AFTER attribute fixing — can modify ONLY attributesfunc textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)Use case for willProcessEditing: Syntax highlighting — detect keywords and apply attributes before layout.
Use case for didProcessEditing: Attribute cleanup — ensure consistent paragraph styles.
NSLayoutManager
Section titled “NSLayoutManager”Translates characters → glyphs, lays out glyphs into line fragments within text containers.
Glyph Generation
Section titled “Glyph Generation”// Force glyph generation for a rangelayoutManager.ensureGlyphs(forCharacterRange: range)layoutManager.ensureGlyphs(forGlyphRange: glyphRange)
// Query glyphslet glyph = layoutManager.glyph(at: glyphIndex)let glyphRange = layoutManager.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil)let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)Character → glyph mapping is NOT 1:1. Ligatures, composed characters, and complex scripts can produce:
- One character → multiple glyphs
- Multiple characters → one glyph (ligatures)
Layout Process
Section titled “Layout Process”// Force layout for specific targetslayoutManager.ensureLayout(for: textContainer)layoutManager.ensureLayout(forCharacterRange: range)layoutManager.ensureLayout(forGlyphRange: glyphRange)layoutManager.ensureLayout(forBoundingRect: rect, in: textContainer)Layout is lazy by default. Glyphs and layout are computed on demand when queried. ensureLayout forces eager computation.
Line Fragment Queries
Section titled “Line Fragment Queries”// Bounding rect for a glyph rangelet rect = layoutManager.boundingRect(forGlyphRange: range, in: textContainer)
// Line fragment rect containing a glyphlet lineRect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveRange)
// Used rect (accounts for line spacing)let usedRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveRange)
// Location of glyph within line fragmentlet point = layoutManager.location(forGlyphAt: glyphIndex)
// Character index at pointlet charIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &fraction)Non-Contiguous Layout (Optional)
Section titled “Non-Contiguous Layout (Optional)”layoutManager.allowsNonContiguousLayout = trueWhen enabled, the layout manager can skip laying out text that isn’t currently visible. Improves performance for large documents but is not always reliable in TextKit 1.
Checking if layout is complete:
if layoutManager.hasNonContiguousLayout { // Some ranges may not be laid out yet}Temporary Attributes
Section titled “Temporary Attributes”Overlay visual attributes without modifying the text storage. Used for spell-check underlines, find highlights, etc.
// Set temporary attributeslayoutManager.setTemporaryAttributes([.foregroundColor: UIColor.red], forCharacterRange: range)
// Add (merge) temporary attributeslayoutManager.addTemporaryAttribute(.backgroundColor, value: UIColor.yellow, forCharacterRange: range)
// Remove temporary attributeslayoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: range)Key difference from text storage attributes: Temporary attributes don’t persist, don’t participate in archiving, and don’t trigger layout invalidation.
Delegate Methods
Section titled “Delegate Methods”// Control line spacingfunc layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
// Control paragraph spacingfunc layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
// Customize line fragment rectfunc layoutManager(_ layoutManager: NSLayoutManager, shouldUse lineFragmentRect: UnsafeMutablePointer<CGRect>, forTextContainer textContainer: NSTextContainer) -> Bool
// Custom glyph drawingfunc layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes: UnsafePointer<Int>, font: UIFont, forGlyphRange glyphRange: NSRange) -> IntNSTextContainer
Section titled “NSTextContainer”Defines the geometric region where text is laid out.
Configuration
Section titled “Configuration”let container = NSTextContainer(size: CGSize(width: 300, height: .greatestFiniteMagnitude))container.lineFragmentPadding = 5.0 // Default: 5.0 (inset from edges)container.maximumNumberOfLines = 0 // 0 = unlimitedcontainer.lineBreakMode = .byWordWrappingExclusion Paths
Section titled “Exclusion Paths”Regions where text should NOT be laid out (e.g., around images):
let circlePath = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100))container.exclusionPaths = [circlePath]Coordinate system: Exclusion paths are in the text container’s coordinate space.
Multi-Column / Multi-Page Layout
Section titled “Multi-Column / Multi-Page Layout”let layoutManager = NSLayoutManager()textStorage.addLayoutManager(layoutManager)
let container1 = NSTextContainer(size: CGSize(width: 300, height: 500))let container2 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager.addTextContainer(container1)layoutManager.addTextContainer(container2)
// Text overflows from container1 into container2let textView1 = UITextView(frame: frame1, textContainer: container1)let textView2 = UITextView(frame: frame2, textContainer: container2)Custom Drawing
Section titled “Custom Drawing”Drawing Glyphs
Section titled “Drawing Glyphs”// Override in NSLayoutManager subclassoverride func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { // Custom background drawing drawCustomBackground(forGlyphRange: glyphsToShow, at: origin) // Default glyph drawing super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)}
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { // Custom background (strikethrough, highlights, etc.) super.drawBackground(forGlyphRange: glyphsToShow, at: origin)}Text Attachments
Section titled “Text Attachments”let attachment = NSTextAttachment()attachment.image = UIImage(named: "icon")attachment.bounds = CGRect(x: 0, y: -4, width: 20, height: 20)
let attrString = NSAttributedString(attachment: attachment)textStorage.insert(attrString, at: insertionPoint)Quick Reference
Section titled “Quick Reference”| Task | API |
|---|---|
| Force glyph generation | ensureGlyphs(forCharacterRange:) |
| Force layout | ensureLayout(for:) / ensureLayout(forCharacterRange:) |
| Character at point | characterIndex(for:in:fractionOfDistanceBetweenInsertionPoints:) |
| Rect for character range | boundingRect(forGlyphRange:in:) |
| Line rect at glyph | lineFragmentRect(forGlyphAt:effectiveRange:) |
| Total used rect | usedRect(for:) |
| Number of glyphs | numberOfGlyphs |
| Glyph ↔ character mapping | glyphRange(forCharacterRange:actualCharacterRange:) |
| Overlay styling | setTemporaryAttributes(_:forCharacterRange:) |
| Invalidate layout | invalidateLayout(forCharacterRange:actualCharacterRange:) |
| Invalidate glyphs | invalidateGlyphs(forCharacterRange:changeInLength:actualCharacterRange:) |
Common Pitfalls
Section titled “Common Pitfalls”- Forgetting
edited()in NSTextStorage subclass — Layout managers never update. Always calledited(_:range:changeInLength:)in mutation primitives. - Modifying characters in
didProcessEditing— Causes crashes or undefined behavior. Only modify attributes. - Not batching edits — Each individual mutation triggers
processEditing(). Wrap inbeginEditing()/endEditing(). - Accessing
textView.layoutManageron TextKit 2 views — Triggers irreversible fallback to TextKit 1. ChecktextLayoutManagerfirst. ensureLayout(for:)on large documents — O(n) operation. UseensureLayout(forBoundingRect:in:)to limit scope.- Assuming 1:1 character-glyph mapping — Complex scripts and ligatures break this assumption.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-textkit1-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-fallback-triggers: Use when the user needs to know exactly what makes TextKit 2 fall back to TextKit 1, or wants to audit code for fallback risk before it ships. Reach for this when the question is specifically about compatibility-mode triggers, not general text-system debugging.
Full SKILL.md source
---name: apple-text-textkit1-refdescription: > 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.license: MIT---
# TextKit 1 Reference
Use this skill when you already know the editor is on TextKit 1 and need the exact APIs or lifecycle details.
## When to Use
- You are working with `NSLayoutManager`.- You need glyph-based APIs.- You are maintaining legacy or explicitly opt-in TextKit 1 code.
## Quick Decision
- Need to choose between TextKit 1 and 2 -> `/skill apple-text-layout-manager-selection`- Already committed to TextKit 1 and need exact APIs -> stay here- Debugging symptoms before you know the root cause -> `/skill apple-text-textkit-diag`
## Core Guidance
Complete reference for TextKit 1 covering the NSLayoutManager-based text system available since iOS 7 / macOS 10.0.
## Architecture (MVC Triad)
```NSTextStorage (Model) ←→ NSLayoutManager (Controller) ←→ NSTextContainer → UITextView/NSTextView (View) │ │ │ Attributed string Glyphs + layout Geometric region Character storage Glyph → character mapping Exclusion paths Edit notifications Line fragment rects Size constraints```
**One-to-many relationships:**- One NSTextStorage → many NSLayoutManagers (same text, different layouts)- One NSLayoutManager → many NSTextContainers (multi-page/multi-column)- One NSTextContainer → one UITextView/NSTextView
## NSTextStorage
Subclass of `NSMutableAttributedString`. The canonical backing store for all TextKit text.
### Required Primitives (When Subclassing)
You **must** subclass NSTextStorage if you want a custom backing store. Implement these four:
```swiftclass CustomTextStorage: NSTextStorage { private var storage = NSMutableAttributedString()
override var string: String { storage.string }
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] { storage.attributes(at: location, effectiveRange: range) }
override func replaceCharacters(in range: NSRange, with str: String) { beginEditing() storage.replaceCharacters(in: range, with: str) edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length) endEditing() }
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) { beginEditing() storage.setAttributes(attrs, range: range) edited(.editedAttributes, range: range, changeInLength: 0) endEditing() }}```
**Critical:** Mutation methods MUST call `edited(_:range:changeInLength:)` with the correct mask (`.editedCharacters`, `.editedAttributes`, or both). Without this, layout managers won't be notified.
### Editing Lifecycle
```beginEditing()├── replaceCharacters(in:with:) → calls edited(.editedCharacters, ...)├── setAttributes(_:range:) → calls edited(.editedAttributes, ...)├── addAttribute(_:value:range:) → calls edited(.editedAttributes, ...)└── endEditing() └── processEditing() ├── delegate.textStorage(_:willProcessEditing:range:changeInLength:) │ └── Can modify BOTH characters AND attributes ├── fixAttributes(in:) — font substitution, paragraph style fixing ├── delegate.textStorage(_:didProcessEditing:range:changeInLength:) │ └── Can modify ONLY attributes (characters → crash/undefined) └── Notifies all attached layout managers └── layoutManager.processEditing(for:edited:range:changeInLength:invalidatedRange:)```
**Batching edits:** Wrap multiple mutations in `beginEditing()`/`endEditing()` to coalesce into one `processEditing()` call. Without batching, each mutation triggers a separate layout invalidation pass.
### Edit Masks
```swiftNSTextStorage.EditActions.editedCharacters // Text content changedNSTextStorage.EditActions.editedAttributes // Attributes changed (no text change)// Combine: [.editedCharacters, .editedAttributes]```
### Delegate Methods
```swift// BEFORE attribute fixing — can modify characters AND attributesfunc textStorage(_ textStorage: NSTextStorage, willProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)
// AFTER attribute fixing — can modify ONLY attributesfunc textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int)```
**Use case for willProcessEditing:** Syntax highlighting — detect keywords and apply attributes before layout.
**Use case for didProcessEditing:** Attribute cleanup — ensure consistent paragraph styles.
## NSLayoutManager
Translates characters → glyphs, lays out glyphs into line fragments within text containers.
### Glyph Generation
```swift// Force glyph generation for a rangelayoutManager.ensureGlyphs(forCharacterRange: range)layoutManager.ensureGlyphs(forGlyphRange: glyphRange)
// Query glyphslet glyph = layoutManager.glyph(at: glyphIndex)let glyphRange = layoutManager.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil)let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)```
**Character → glyph mapping is NOT 1:1.** Ligatures, composed characters, and complex scripts can produce:- One character → multiple glyphs- Multiple characters → one glyph (ligatures)
### Layout Process
```swift// Force layout for specific targetslayoutManager.ensureLayout(for: textContainer)layoutManager.ensureLayout(forCharacterRange: range)layoutManager.ensureLayout(forGlyphRange: glyphRange)layoutManager.ensureLayout(forBoundingRect: rect, in: textContainer)```
**Layout is lazy by default.** Glyphs and layout are computed on demand when queried. `ensureLayout` forces eager computation.
### Line Fragment Queries
```swift// Bounding rect for a glyph rangelet rect = layoutManager.boundingRect(forGlyphRange: range, in: textContainer)
// Line fragment rect containing a glyphlet lineRect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveRange)
// Used rect (accounts for line spacing)let usedRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveRange)
// Location of glyph within line fragmentlet point = layoutManager.location(forGlyphAt: glyphIndex)
// Character index at pointlet charIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &fraction)```
### Non-Contiguous Layout (Optional)
```swiftlayoutManager.allowsNonContiguousLayout = true```
When enabled, the layout manager can skip laying out text that isn't currently visible. Improves performance for large documents but is **not always reliable** in TextKit 1.
**Checking if layout is complete:**```swiftif layoutManager.hasNonContiguousLayout { // Some ranges may not be laid out yet}```
### Temporary Attributes
Overlay visual attributes without modifying the text storage. Used for spell-check underlines, find highlights, etc.
```swift// Set temporary attributeslayoutManager.setTemporaryAttributes([.foregroundColor: UIColor.red], forCharacterRange: range)
// Add (merge) temporary attributeslayoutManager.addTemporaryAttribute(.backgroundColor, value: UIColor.yellow, forCharacterRange: range)
// Remove temporary attributeslayoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: range)```
**Key difference from text storage attributes:** Temporary attributes don't persist, don't participate in archiving, and don't trigger layout invalidation.
### Delegate Methods
```swift// Control line spacingfunc layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
// Control paragraph spacingfunc layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat
// Customize line fragment rectfunc layoutManager(_ layoutManager: NSLayoutManager, shouldUse lineFragmentRect: UnsafeMutablePointer<CGRect>, forTextContainer textContainer: NSTextContainer) -> Bool
// Custom glyph drawingfunc layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes: UnsafePointer<Int>, font: UIFont, forGlyphRange glyphRange: NSRange) -> Int```
## NSTextContainer
Defines the geometric region where text is laid out.
### Configuration
```swiftlet container = NSTextContainer(size: CGSize(width: 300, height: .greatestFiniteMagnitude))container.lineFragmentPadding = 5.0 // Default: 5.0 (inset from edges)container.maximumNumberOfLines = 0 // 0 = unlimitedcontainer.lineBreakMode = .byWordWrapping```
### Exclusion Paths
Regions where text should NOT be laid out (e.g., around images):
```swiftlet circlePath = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100))container.exclusionPaths = [circlePath]```
**Coordinate system:** Exclusion paths are in the text container's coordinate space.
### Multi-Column / Multi-Page Layout
```swiftlet layoutManager = NSLayoutManager()textStorage.addLayoutManager(layoutManager)
let container1 = NSTextContainer(size: CGSize(width: 300, height: 500))let container2 = NSTextContainer(size: CGSize(width: 300, height: 500))layoutManager.addTextContainer(container1)layoutManager.addTextContainer(container2)
// Text overflows from container1 into container2let textView1 = UITextView(frame: frame1, textContainer: container1)let textView2 = UITextView(frame: frame2, textContainer: container2)```
## Custom Drawing
### Drawing Glyphs
```swift// Override in NSLayoutManager subclassoverride func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { // Custom background drawing drawCustomBackground(forGlyphRange: glyphsToShow, at: origin) // Default glyph drawing super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)}
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { // Custom background (strikethrough, highlights, etc.) super.drawBackground(forGlyphRange: glyphsToShow, at: origin)}```
### Text Attachments
```swiftlet attachment = NSTextAttachment()attachment.image = UIImage(named: "icon")attachment.bounds = CGRect(x: 0, y: -4, width: 20, height: 20)
let attrString = NSAttributedString(attachment: attachment)textStorage.insert(attrString, at: insertionPoint)```
## Quick Reference
| Task | API ||------|-----|| Force glyph generation | `ensureGlyphs(forCharacterRange:)` || Force layout | `ensureLayout(for:)` / `ensureLayout(forCharacterRange:)` || Character at point | `characterIndex(for:in:fractionOfDistanceBetweenInsertionPoints:)` || Rect for character range | `boundingRect(forGlyphRange:in:)` || Line rect at glyph | `lineFragmentRect(forGlyphAt:effectiveRange:)` || Total used rect | `usedRect(for:)` || Number of glyphs | `numberOfGlyphs` || Glyph ↔ character mapping | `glyphRange(forCharacterRange:actualCharacterRange:)` || Overlay styling | `setTemporaryAttributes(_:forCharacterRange:)` || Invalidate layout | `invalidateLayout(forCharacterRange:actualCharacterRange:)` || Invalidate glyphs | `invalidateGlyphs(forCharacterRange:changeInLength:actualCharacterRange:)` |
## Common Pitfalls
1. **Forgetting `edited()` in NSTextStorage subclass** — Layout managers never update. Always call `edited(_:range:changeInLength:)` in mutation primitives.2. **Modifying characters in `didProcessEditing`** — Causes crashes or undefined behavior. Only modify attributes.3. **Not batching edits** — Each individual mutation triggers `processEditing()`. Wrap in `beginEditing()`/`endEditing()`.4. **Accessing `textView.layoutManager` on TextKit 2 views** — Triggers irreversible fallback to TextKit 1. Check `textLayoutManager` first.5. **`ensureLayout(for:)` on large documents** — O(n) operation. Use `ensureLayout(forBoundingRect:in:)` to limit scope.6. **Assuming 1:1 character-glyph mapping** — Complex scripts and ligatures break this assumption.
## Related Skills
- Use `/skill apple-text-layout-manager-selection` for migration or stack choice.- Use `/skill apple-text-fallback-triggers` when TextKit 1 appears unexpectedly.- Use `/skill apple-text-storage` for deeper storage-layer behavior underneath the glyph APIs.