TextKit 1 Fallback Triggers — Complete Catalog
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.
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.
Family: TextKit Runtime And Layout
Use this skill when the main question is why a TextKit 2 view entered compatibility mode or how to avoid doing that.
When to Use
Section titled “When to Use”textLayoutManagerunexpectedly becomesnil- Writing Tools loses inline behavior
- You need to audit fallback risk before touching a text view
Quick Decision
Section titled “Quick Decision”- Need a symptom-first debugger ->
/skill apple-text-textkit-diag - Need the exact fallback trigger catalog -> stay here
- Need to choose TextKit 1 on purpose ->
/skill apple-text-layout-manager-selection
Core Guidance
Section titled “Core Guidance”TextKit 2 falls back to TextKit 1 permanently and irreversibly on a given text view instance. Once textLayoutManager returns nil, there is no way back. This skill catalogs every known trigger.
The Fallback Mechanism
Section titled “The Fallback Mechanism”When triggered, the text view:
- Replaces
NSTextLayoutManagerwithNSLayoutManager textLayoutManagerreturnsnilpermanently- All cached TextKit 2 objects stop functioning
- View-based
NSTextAttachmentViewProviderattachments are instantly lost - Writing Tools degrades to panel-only mode
- Viewport-based layout optimization is lost
Category 1: Explicit NSLayoutManager Access (Most Common)
Section titled “Category 1: Explicit NSLayoutManager Access (Most Common)”| Trigger | Why It Causes Fallback |
|---|---|
textView.layoutManager | Forces TextKit 1 infrastructure creation |
textView.textContainer.layoutManager | Same — accesses TK1 layout manager |
textStorage.addLayoutManager(_:) | Adds TK1 layout manager to storage |
textStorage.removeLayoutManager(_:) | Manipulates TK1 layout manager list |
textContainer.replaceLayoutManager(_:) | Swaps in TK1 layout manager |
// ❌ TRIGGERS FALLBACK — even a read-only checkif textView.layoutManager != nil { ... }if let lm = textView.textContainer.layoutManager { ... }
// ✅ SAFE — check TextKit 2 firstif let tlm = textView.textLayoutManager { // TextKit 2 path} else { // Already in TextKit 1 — safe to use layoutManager let lm = textView.layoutManager}Category 2: Any Glyph-Based API
Section titled “Category 2: Any Glyph-Based API”TextKit 2 has zero glyph APIs. Any glyph access requires TextKit 1:
| API | TextKit 2 Alternative |
|---|---|
numberOfGlyphs | Enumerate layout fragments |
glyph(at:) | No equivalent — use Core Text directly |
glyphRange(for:) | enumerateTextLayoutFragments |
lineFragmentRect(forGlyphAt:) | textLineFragments[n].typographicBounds |
boundingRect(forGlyphRange:in:) | Union of layout fragment frames |
characterIndex(for:in:fractionOf...) | location(interactingAt:inContainerAt:) |
drawGlyphs(forGlyphRange:at:) | NSTextLayoutFragment.draw(at:in:) subclass |
drawBackground(forGlyphRange:at:) | Custom layout fragment |
shouldGenerateGlyphs delegate | No equivalent — customize at fragment level |
Category 3: Unsupported Attributes
Section titled “Category 3: Unsupported Attributes”| Attribute | Status | Notes |
|---|---|---|
| NSTextTable / NSTextTableBlock | Triggers fallback | AppKit-only. Apple’s TextEdit falls back for tables |
| NSTextList | Partially supported | Supported since iOS 17/macOS 14. Earlier versions may fall back |
| NSTextAttachment (TK1 cell API) | Can trigger fallback | attachmentBounds(for:proposedLineFragment:glyphPosition:characterIndex:) crashes on iOS 16.0. Use NSTextAttachmentViewProvider instead |
| NSTextAttachmentCell | Triggers fallback | TextKit 1 only protocol. Use NSTextAttachmentViewProvider for TextKit 2 |
Category 4: Multi-Container Layout
Section titled “Category 4: Multi-Container Layout”TextKit 2’s NSTextLayoutManager supports only ONE text container.
| Pattern | Fallback? |
|---|---|
Multiple NSTextContainer on one layout manager | Requires TextKit 1 |
| Multi-page / multi-column layout | Requires TextKit 1 |
| ”Wrap to Page” in TextEdit | Falls back to TextKit 1 |
Category 5: Printing
Section titled “Category 5: Printing”| OS Version | Printing Support |
|---|---|
| Before macOS 15 / iOS 18 | No printing in TextKit 2 — triggers fallback |
| macOS 15+ / iOS 18+ | Basic printing supported, limited pagination — NSTextLayoutManager still only supports a single NSTextContainer, so multi-page layout requires TextKit 1. Apple’s TextEdit still falls back to TextKit 1 for printing. |
Category 6: Framework-Internal Fallbacks
Section titled “Category 6: Framework-Internal Fallbacks”These happen without YOUR code accessing layoutManager:
- UIKit/AppKit framework internals sometimes access
layoutManagerinternally - Undocumented and varies between OS releases
- Apple recommends filing Feedback Assistant reports for these
- Third-party libraries accessing
layoutManageron your text view
Quote from STTextView author: “You never know what might trigger that fallback, and the cases are not documented and will vary from release to release.”
Category 7: NSTextView-Specific (macOS)
Section titled “Category 7: NSTextView-Specific (macOS)”| Trigger | Notes |
|---|---|
| Quick Look preview of attachments | Bug in macOS 14 and earlier |
drawInsertionPoint(in:color:turnedOn:) override | Doesn’t trigger fallback but silently stops working under TextKit 2 |
Any NSTextField accessing field editor’s layoutManager | Falls back ALL field editors in that window |
What Does NOT Cause Fallback
Section titled “What Does NOT Cause Fallback”This is equally important — these are safe to use with TextKit 2:
NSTextStorage Is the Normal Backing Store
Section titled “NSTextStorage Is the Normal Backing Store”NSTextContentStorage wraps NSTextStorage. This is the standard architecture.
// ✅ SAFE — accessing the backing store through content storagelet textStorage = textContentStorage.textStorage
// ✅ SAFE — editing through the content storagetextContentStorage.performEditingTransaction { textStorage?.replaceCharacters(in: range, with: newText)}
// ✅ SAFE — NSTextStorage subclass works with TextKit 2class MyStorage: NSTextStorage { ... }let contentStorage = NSTextContentStorage()contentStorage.textStorage = MyStorage()The distinction: “Fallback” means the layout system switches from NSTextLayoutManager to NSLayoutManager. The storage layer (NSTextStorage) is ALWAYS present — it’s the backing store for both systems.
Safe Properties and Methods
Section titled “Safe Properties and Methods”| Property/Method | Safe? | Notes |
|---|---|---|
textView.textLayoutManager | ✅ | Returns nil if already TK1 |
textView.textStorage (UITextView) | ✅ | Direct storage access is fine |
textContainer.exclusionPaths | ✅ | Supported since iOS 16 |
textContainerInset | ✅ | |
typingAttributes | ✅ | |
selectedRange / selectedTextRange | ✅ | |
All UITextViewDelegate methods | ✅ | |
| Standard attributed string attributes | ✅ | font, color, paragraph style, etc. |
NSTextContentStorage.performEditingTransaction | ✅ | Preferred edit wrapper |
NSTextStorage.beginEditing/endEditing | ✅ | When wrapped in transaction |
NSTextStorage Subclass with TextKit 2
Section titled “NSTextStorage Subclass with TextKit 2”A custom NSTextStorage subclass works with TextKit 2 when:
- Used as the backing store of
NSTextContentStorage - All edits go through
performEditingTransaction - The four primitives are correctly implemented
- You never access
layoutManageron the text view
// ✅ Custom backing store with TextKit 2class RopeTextStorage: NSTextStorage { // ... implement 4 primitives with edited() calls}
let contentStorage = NSTextContentStorage()contentStorage.textStorage = RopeTextStorage()// The text view uses NSTextLayoutManager — no fallbackCannot do: Custom NSTextContentManager subclass (without NSTextStorage) — causes crashes. Custom NSTextElement subclasses beyond NSTextParagraph — triggers runtime assertions.
How to Detect Fallback
Section titled “How to Detect Fallback”UIKit (iOS)
Section titled “UIKit (iOS)”// Runtime checkif textView.textLayoutManager == nil { print("⚠️ TextKit 1 mode (fallback occurred or was never TK2)")}
// Symbolic breakpoint (Xcode)// Symbol: _UITextViewEnablingCompatibilityMode// Action: Log message with backtrace to find the triggerAppKit (macOS)
Section titled “AppKit (macOS)”// NotificationsNotificationCenter.default.addObserver( forName: NSTextView.willSwitchToNSLayoutManagerNotification, object: textView, queue: .main) { _ in print("⚠️ About to fall back — check call stack")}
NotificationCenter.default.addObserver( forName: NSTextView.didSwitchToNSLayoutManagerNotification, object: textView, queue: .main) { _ in print("⚠️ Fell back to TextKit 1")}Console Log
Section titled “Console Log”The system logs: "UITextView <addr> is switching to TextKit 1 compatibility mode because its layoutManager was accessed"
How to Opt Out (Use TextKit 1 from Start)
Section titled “How to Opt Out (Use TextKit 1 from Start)”If you NEED TextKit 1, don’t create a TextKit 2 view and let it fall back — that wastes initialization:
// ✅ CORRECT — explicit TextKit 1 from startlet textView = UITextView(usingTextLayoutManager: false)
// ✅ CORRECT — manual TextKit 1 setuplet storage = NSTextStorage()let layoutManager = NSLayoutManager()storage.addLayoutManager(layoutManager)let container = NSTextContainer(size: CGSize(width: 300, height: .greatestFiniteMagnitude))layoutManager.addTextContainer(container)let textView = UITextView(frame: .zero, textContainer: container)Recovery from Fallback
Section titled “Recovery from Fallback”There is no recovery on the same instance. To get back to TextKit 2:
- Create a NEW text view with TextKit 2
- Transfer the text content (attributedText)
- Replace the old view in the hierarchy
- Re-wire delegates and observers
Fallback Improvement Timeline
Section titled “Fallback Improvement Timeline”| OS | TextKit 2 Improvement |
|---|---|
| iOS 15 / macOS 12 | TextKit 2 introduced (opt-in) |
| iOS 16 / macOS 13 | Default for all text controls; compatibility mode added |
| iOS 17 / macOS 14 | NSTextList support; CJK line-breaking improvements |
| iOS 18 / macOS 15 | Printing support added |
| macOS 26 | includesTextListMarkers property; NSTextViewAllowsDowngradeToLayoutManager user default |
Trend: Each OS release supports more features in TextKit 2, reducing fallback triggers. But multi-container layout and text tables remain TextKit 1 only.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-fallback-triggers diagnostic skill. Use it when broken behavior, regressions, or symptoms are the starting point.
Related
Section titled “Related”apple-text-textkit-diag: Use when the user starts with a broken Apple text symptom such as stale layout, fallback, crashes in editing, rendering artifacts, missing Writing Tools, or large-document slowness. Reach for this when debugging misbehavior, not when reviewing code systematically or looking up APIs.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-audit: Use when the user wants a review-style scan of Apple text code for risks such as TextKit fallback, editing lifecycle bugs, deprecated APIs, performance traps, or Writing Tools breakage. Reach for this when the job is findings from real code, not a symptom-first debug answer or direct API lookup.
Full SKILL.md source
---name: apple-text-fallback-triggersdescription: 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.license: MIT---
# TextKit 1 Fallback Triggers — Complete Catalog
Use this skill when the main question is why a TextKit 2 view entered compatibility mode or how to avoid doing that.
## When to Use
- `textLayoutManager` unexpectedly becomes `nil`- Writing Tools loses inline behavior- You need to audit fallback risk before touching a text view
## Quick Decision
- Need a symptom-first debugger -> `/skill apple-text-textkit-diag`- Need the exact fallback trigger catalog -> stay here- Need to choose TextKit 1 on purpose -> `/skill apple-text-layout-manager-selection`
## Core Guidance
TextKit 2 falls back to TextKit 1 **permanently and irreversibly** on a given text view instance. Once `textLayoutManager` returns `nil`, there is no way back. This skill catalogs every known trigger.
## The Fallback Mechanism
When triggered, the text view:1. Replaces `NSTextLayoutManager` with `NSLayoutManager`2. `textLayoutManager` returns `nil` permanently3. All cached TextKit 2 objects stop functioning4. View-based `NSTextAttachmentViewProvider` attachments are **instantly lost**5. Writing Tools degrades to panel-only mode6. Viewport-based layout optimization is lost
## Category 1: Explicit NSLayoutManager Access (Most Common)
| Trigger | Why It Causes Fallback ||---------|----------------------|| `textView.layoutManager` | Forces TextKit 1 infrastructure creation || `textView.textContainer.layoutManager` | Same — accesses TK1 layout manager || `textStorage.addLayoutManager(_:)` | Adds TK1 layout manager to storage || `textStorage.removeLayoutManager(_:)` | Manipulates TK1 layout manager list || `textContainer.replaceLayoutManager(_:)` | Swaps in TK1 layout manager |
```swift// ❌ TRIGGERS FALLBACK — even a read-only checkif textView.layoutManager != nil { ... }if let lm = textView.textContainer.layoutManager { ... }
// ✅ SAFE — check TextKit 2 firstif let tlm = textView.textLayoutManager { // TextKit 2 path} else { // Already in TextKit 1 — safe to use layoutManager let lm = textView.layoutManager}```
## Category 2: Any Glyph-Based API
TextKit 2 has **zero glyph APIs**. Any glyph access requires TextKit 1:
| API | TextKit 2 Alternative ||-----|----------------------|| `numberOfGlyphs` | Enumerate layout fragments || `glyph(at:)` | No equivalent — use Core Text directly || `glyphRange(for:)` | `enumerateTextLayoutFragments` || `lineFragmentRect(forGlyphAt:)` | `textLineFragments[n].typographicBounds` || `boundingRect(forGlyphRange:in:)` | Union of layout fragment frames || `characterIndex(for:in:fractionOf...)` | `location(interactingAt:inContainerAt:)` || `drawGlyphs(forGlyphRange:at:)` | `NSTextLayoutFragment.draw(at:in:)` subclass || `drawBackground(forGlyphRange:at:)` | Custom layout fragment || `shouldGenerateGlyphs` delegate | No equivalent — customize at fragment level |
## Category 3: Unsupported Attributes
| Attribute | Status | Notes ||-----------|--------|-------|| **NSTextTable / NSTextTableBlock** | Triggers fallback | AppKit-only. Apple's TextEdit falls back for tables || **NSTextList** | Partially supported | Supported since iOS 17/macOS 14. Earlier versions may fall back || **NSTextAttachment (TK1 cell API)** | Can trigger fallback | `attachmentBounds(for:proposedLineFragment:glyphPosition:characterIndex:)` crashes on iOS 16.0. Use `NSTextAttachmentViewProvider` instead || **NSTextAttachmentCell** | Triggers fallback | TextKit 1 only protocol. Use `NSTextAttachmentViewProvider` for TextKit 2 |
## Category 4: Multi-Container Layout
**TextKit 2's NSTextLayoutManager supports only ONE text container.**
| Pattern | Fallback? ||---------|-----------|| Multiple `NSTextContainer` on one layout manager | Requires TextKit 1 || Multi-page / multi-column layout | Requires TextKit 1 || "Wrap to Page" in TextEdit | Falls back to TextKit 1 |
## Category 5: Printing
| OS Version | Printing Support ||------------|-----------------|| Before macOS 15 / iOS 18 | **No printing in TextKit 2** — triggers fallback || macOS 15+ / iOS 18+ | Basic printing supported, limited pagination — `NSTextLayoutManager` still only supports a single `NSTextContainer`, so multi-page layout requires TextKit 1. Apple's TextEdit still falls back to TextKit 1 for printing. |
## Category 6: Framework-Internal Fallbacks
**These happen without YOUR code accessing layoutManager:**
- UIKit/AppKit framework internals sometimes access `layoutManager` internally- Undocumented and **varies between OS releases**- Apple recommends filing Feedback Assistant reports for these- Third-party libraries accessing `layoutManager` on your text view
**Quote from STTextView author:** *"You never know what might trigger that fallback, and the cases are not documented and will vary from release to release."*
## Category 7: NSTextView-Specific (macOS)
| Trigger | Notes ||---------|-------|| Quick Look preview of attachments | Bug in macOS 14 and earlier || `drawInsertionPoint(in:color:turnedOn:)` override | Doesn't trigger fallback but **silently stops working** under TextKit 2 || Any NSTextField accessing field editor's `layoutManager` | Falls back ALL field editors in that window |
## What Does NOT Cause Fallback
This is equally important — these are **safe** to use with TextKit 2:
### NSTextStorage Is the Normal Backing Store
**NSTextContentStorage wraps NSTextStorage. This is the standard architecture.**
```swift// ✅ SAFE — accessing the backing store through content storagelet textStorage = textContentStorage.textStorage
// ✅ SAFE — editing through the content storagetextContentStorage.performEditingTransaction { textStorage?.replaceCharacters(in: range, with: newText)}
// ✅ SAFE — NSTextStorage subclass works with TextKit 2class MyStorage: NSTextStorage { ... }let contentStorage = NSTextContentStorage()contentStorage.textStorage = MyStorage()```
**The distinction:** "Fallback" means the layout system switches from `NSTextLayoutManager` to `NSLayoutManager`. The storage layer (NSTextStorage) is ALWAYS present — it's the backing store for both systems.
### Safe Properties and Methods
| Property/Method | Safe? | Notes ||----------------|-------|-------|| `textView.textLayoutManager` | ✅ | Returns nil if already TK1 || `textView.textStorage` (UITextView) | ✅ | Direct storage access is fine || `textContainer.exclusionPaths` | ✅ | Supported since iOS 16 || `textContainerInset` | ✅ | || `typingAttributes` | ✅ | || `selectedRange` / `selectedTextRange` | ✅ | || All `UITextViewDelegate` methods | ✅ | || Standard attributed string attributes | ✅ | font, color, paragraph style, etc. || `NSTextContentStorage.performEditingTransaction` | ✅ | Preferred edit wrapper || `NSTextStorage.beginEditing`/`endEditing` | ✅ | When wrapped in transaction |
### NSTextStorage Subclass with TextKit 2
A custom NSTextStorage subclass **works with TextKit 2** when:1. Used as the backing store of `NSTextContentStorage`2. All edits go through `performEditingTransaction`3. The four primitives are correctly implemented4. You never access `layoutManager` on the text view
```swift// ✅ Custom backing store with TextKit 2class RopeTextStorage: NSTextStorage { // ... implement 4 primitives with edited() calls}
let contentStorage = NSTextContentStorage()contentStorage.textStorage = RopeTextStorage()// The text view uses NSTextLayoutManager — no fallback```
**Cannot do:** Custom `NSTextContentManager` subclass (without NSTextStorage) — causes crashes. Custom `NSTextElement` subclasses beyond `NSTextParagraph` — triggers runtime assertions.
## How to Detect Fallback
### UIKit (iOS)
```swift// Runtime checkif textView.textLayoutManager == nil { print("⚠️ TextKit 1 mode (fallback occurred or was never TK2)")}
// Symbolic breakpoint (Xcode)// Symbol: _UITextViewEnablingCompatibilityMode// Action: Log message with backtrace to find the trigger```
### AppKit (macOS)
```swift// NotificationsNotificationCenter.default.addObserver( forName: NSTextView.willSwitchToNSLayoutManagerNotification, object: textView, queue: .main) { _ in print("⚠️ About to fall back — check call stack")}
NotificationCenter.default.addObserver( forName: NSTextView.didSwitchToNSLayoutManagerNotification, object: textView, queue: .main) { _ in print("⚠️ Fell back to TextKit 1")}```
### Console Log
The system logs: `"UITextView <addr> is switching to TextKit 1 compatibility mode because its layoutManager was accessed"`
## How to Opt Out (Use TextKit 1 from Start)
If you NEED TextKit 1, don't create a TextKit 2 view and let it fall back — that wastes initialization:
```swift// ✅ CORRECT — explicit TextKit 1 from startlet textView = UITextView(usingTextLayoutManager: false)
// ✅ CORRECT — manual TextKit 1 setuplet storage = NSTextStorage()let layoutManager = NSLayoutManager()storage.addLayoutManager(layoutManager)let container = NSTextContainer(size: CGSize(width: 300, height: .greatestFiniteMagnitude))layoutManager.addTextContainer(container)let textView = UITextView(frame: .zero, textContainer: container)```
## Recovery from Fallback
**There is no recovery on the same instance.** To get back to TextKit 2:
1. Create a NEW text view with TextKit 22. Transfer the text content (attributedText)3. Replace the old view in the hierarchy4. Re-wire delegates and observers
## Fallback Improvement Timeline
| OS | TextKit 2 Improvement ||----|----------------------|| iOS 15 / macOS 12 | TextKit 2 introduced (opt-in) || iOS 16 / macOS 13 | Default for all text controls; compatibility mode added || iOS 17 / macOS 14 | NSTextList support; CJK line-breaking improvements || iOS 18 / macOS 15 | Printing support added || macOS 26 | `includesTextListMarkers` property; `NSTextViewAllowsDowngradeToLayoutManager` user default |
**Trend:** Each OS release supports more features in TextKit 2, reducing fallback triggers. But multi-container layout and text tables remain TextKit 1 only.
## Related Skills
- Use `/skill apple-text-textkit-diag` for broader debugging around fallback symptoms.- Use `/skill apple-text-layout-manager-selection` when compatibility mode pressure means TextKit 1 may be the right explicit choice.- Use `/skill apple-text-audit` when you want repository findings ranked by severity.