Skip to content

Writing Tools Integration

Use when integrating Writing Tools — writingToolsBehavior, UIWritingToolsCoordinator, protected ranges, or inline vs panel mode.

Workflow Skills

Use when integrating Writing Tools — writingToolsBehavior, UIWritingToolsCoordinator, protected ranges, or inline vs panel mode.

Family: Editor Features And Interaction

Use this skill when the main question is how Writing Tools should integrate with a native or custom editor.

  • You are integrating Writing Tools into UITextView, NSTextView, or a custom text engine.
  • You need protected ranges, activity lifecycle hooks, or coordinator APIs.
  • Writing Tools appears in the wrong mode or not at all.
  • Native TextKit text view with standard behavior -> stay with native integration
  • Custom UITextInput-based editor -> use the custom view path
  • Fully custom text engine on current systems -> use UIWritingToolsCoordinator

UITextView and NSTextView get Writing Tools automatically. Configure behavior:

// Full inline experience (default) — proofreading marks, inline rewrites
textView.writingToolsBehavior = .default
// Panel-only — results shown in popover, no inline marks
textView.writingToolsBehavior = .limited
// Disable completely
textView.writingToolsBehavior = .none
// What content types Writing Tools can process
textView.writingToolsAllowedInputOptions = [.plainText] // Plain text only
textView.writingToolsAllowedInputOptions = [.plainText, .richText] // Rich text
textView.writingToolsAllowedInputOptions = [.plainText, .richText, .table] // Including tables

Writing Tools requires TextKit 2 for the full inline experience. TextKit 1 views only get the limited panel-based experience (no inline proofreading marks).

// ✅ Gets full inline Writing Tools
let textView = UITextView(usingTextLayoutManager: true)
// ❌ Only gets panel-based Writing Tools
let textView = UITextView(usingTextLayoutManager: false)

Check: If Writing Tools appears only in a popover (no inline marks), verify the view is using TextKit 2.

func textViewWritingToolsWillBegin(_ textView: UITextView) {
// Pause operations that could conflict:
// - Undo coalescing
// - Syncing to server
// - Collaborative editing updates
}
func textViewWritingToolsDidEnd(_ textView: UITextView) {
// Resume normal operations
}
if textView.isWritingToolsActive {
// Writing Tools is running — don't modify text
} else {
// Safe to manipulate text
}

Exclude ranges from Writing Tools rewriting (code blocks, quotes, citations):

func textView(_ textView: UITextView,
writingToolsIgnoredRangesIn enclosingRange: NSRange) -> [NSRange] {
// Return ranges that should NOT be rewritten
var protectedRanges: [NSRange] = []
// Find code blocks
let codePattern = try! NSRegularExpression(pattern: "```[\\s\\S]*?```")
let matches = codePattern.matches(in: textView.text, range: enclosingRange)
protectedRanges.append(contentsOf: matches.map(\.range))
return protectedRanges
}
func textViewWritingToolsWillBegin(_ notification: Notification)
func textViewWritingToolsDidEnd(_ notification: Notification)
// Protected ranges
func textView(_ textView: NSTextView,
writingToolsIgnoredRangesIn range: NSRange) -> [NSRange]

For views using UITextInput (not UITextView), Writing Tools is available through the callout bar if the view adopts UITextInteraction:

class CustomTextView: UIView, UITextInput {
let textInteraction = UITextInteraction(for: .editable)
override init(frame: CGRect) {
super.init(frame: frame)
textInteraction.textInput = self
addInteraction(textInteraction)
// Writing Tools appears in callout bar automatically
}
}

Alternative: Adopt UITextSelectionDisplayInteraction + UIEditMenuInteraction separately.

For fully custom text engines (not using UITextInput), the coordinator provides direct Writing Tools integration with animation support.

class CustomEditorView: UIView {
var coordinator: UIWritingToolsCoordinator!
override init(frame: CGRect) {
super.init(frame: frame)
coordinator = UIWritingToolsCoordinator(delegate: self)
addInteraction(coordinator)
}
}

All methods are async:

extension CustomEditorView: UIWritingToolsCoordinatorDelegate {
// Provide text content for Writing Tools to process
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
requestsContextFor ranges: [UIWritingToolsCoordinator.TextRange]
) async -> UIWritingToolsCoordinator.Context {
let attributedString = getAttributedString(for: ranges)
return UIWritingToolsCoordinator.Context(
attributedString: attributedString,
range: NSRange(location: 0, length: attributedString.length)
)
}
// Handle text replacement
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
replaceRange range: UIWritingToolsCoordinator.TextRange,
with attributedString: NSAttributedString,
reason: UIWritingToolsCoordinator.TextReplacementReason
) async {
applyReplacement(attributedString, in: range)
}
// State changes
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
didChangeState newState: UIWritingToolsCoordinator.State
) {
switch newState {
case .idle:
resumeNormalEditing()
case .nonInteractive:
pauseEditing()
case .interactiveStreaming:
showStreamingUI()
@unknown default:
break
}
}
}

The coordinator supports animated text transitions:

// Provide preview for animation (text snapshot before change)
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
requestsPreviewFor range: UIWritingToolsCoordinator.TextRange
) async -> UIWritingToolsCoordinator.TextPreview {
let rect = getRect(for: range)
return UIWritingToolsCoordinator.TextPreview(
textView: self,
rect: rect
)
}
// Provide proofreading mark paths (underline positions)
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
requestsUnderlinePathFor range: UIWritingToolsCoordinator.TextRange
) async -> UIBezierPath {
return getUnderlinePath(for: range)
}

macOS equivalent with the same delegate pattern:

let coordinator = NSWritingToolsCoordinator(delegate: self)
view.addInteraction(coordinator)

For macOS views without UITextInput-equivalent, implement NSServicesMenuRequestor:

// In NSView or NSViewController
override func validRequestor(forSendType sendType: NSPasteboard.PasteboardType?,
returnType: NSPasteboard.PasteboardType?) -> Any? {
if sendType == .string || sendType == .rtf {
return self
}
return super.validRequestor(forSendType: sendType, returnType: returnType)
}
func writeSelection(to pboard: NSPasteboard, types: [NSPasteboard.PasteboardType]) -> Bool {
pboard.writeObjects([selectedText as NSString])
return true
}
func readSelection(from pboard: NSPasteboard) -> Bool {
guard let string = pboard.string(forType: .string) else { return false }
replaceSelection(with: string)
return true
}

Mark text structure for better Writing Tools understanding:

var str = AttributedString("My Document")
// Mark as heading
str.presentationIntent = .header(level: 1)
// Mark as code block (Writing Tools will protect this)
codeBlock.presentationIntent = .codeBlock(languageHint: "swift")
// Mark as block quote
quote.presentationIntent = .blockQuote
Is your view UITextView or NSTextView?
YES → Set writingToolsBehavior + delegate methods. Done.
NO → Does it conform to UITextInput?
YES → Add UITextInteraction. Writing Tools in callout bar.
NO → iOS 26+?
YES → Use UIWritingToolsCoordinator
NO → Not directly supported. Consider wrapping in UITextView.
SymptomCauseFix
Writing Tools not in menuwritingToolsBehavior = .none or Apple Intelligence not enabledSet .default; user must enable Apple Intelligence in Settings
Only panel mode, no inlineTextKit 1 mode or fallbackEnsure TextKit 2; check for layoutManager access triggering fallback
Writing Tools rewrites codeNo protected rangesImplement writingToolsIgnoredRangesIn delegate
Inline marks not animatingMissing coordinatorUse UIWritingToolsCoordinator (iOS 26+)
Text corrupted after rewriteEditing during active sessionCheck isWritingToolsActive before modifications
  1. TextKit 1 fallback kills inline Writing Tools — Any access to layoutManager triggers fallback. Use TextKit 2.
  2. Not pausing operations during Writing Tools — Server syncs, undo coalescing, etc. can conflict. Use willBegin/didEnd.
  3. Editing text while Writing Tools is active — Check isWritingToolsActive before programmatic text changes.
  4. Forgetting protected ranges — Code blocks, quotes, and citations should be excluded from rewriting.
  5. Assuming Writing Tools is always available — It requires Apple Intelligence enabled. Check availability gracefully.

This page documents the apple-text-writing-tools workflow skill. Use it when the job is a guided review, implementation flow, or integration pass instead of a single API lookup.

  • apple-text: Use when the user has an Apple text-system problem but the right specialist skill is not obvious, or when the request mixes multiple text subsystems.
  • apple-text-textkit2-ref: Use when working with TextKit 2 and you need NSTextLayoutManager or NSTextContentManager APIs — viewport layout, fragments, rendering attributes.
  • apple-text-fallback-triggers: Use when debugging or preventing TextKit 2 fallback to TextKit 1 — complete trigger catalog, detection, and recovery.
Full SKILL.md source
SKILL.md
---
name: apple-text-writing-tools
description: Use when integrating Writing Tools — writingToolsBehavior, UIWritingToolsCoordinator, protected ranges, or inline vs panel mode
license: MIT
---
# Writing Tools Integration
Use this skill when the main question is how Writing Tools should integrate with a native or custom editor.
## When to Use
- You are integrating Writing Tools into `UITextView`, `NSTextView`, or a custom text engine.
- You need protected ranges, activity lifecycle hooks, or coordinator APIs.
- Writing Tools appears in the wrong mode or not at all.
## Quick Decision
- Native TextKit text view with standard behavior -> stay with native integration
- Custom `UITextInput`-based editor -> use the custom view path
- Fully custom text engine on current systems -> use `UIWritingToolsCoordinator`
## Core Guidance
## Native Text View Integration
UITextView and NSTextView get Writing Tools automatically. Configure behavior:
### Behavior Modes
```swift
// Full inline experience (default) — proofreading marks, inline rewrites
textView.writingToolsBehavior = .default
// Panel-only — results shown in popover, no inline marks
textView.writingToolsBehavior = .limited
// Disable completely
textView.writingToolsBehavior = .none
```
### Allowed Input Options
```swift
// What content types Writing Tools can process
textView.writingToolsAllowedInputOptions = [.plainText] // Plain text only
textView.writingToolsAllowedInputOptions = [.plainText, .richText] // Rich text
textView.writingToolsAllowedInputOptions = [.plainText, .richText, .table] // Including tables
```
### TextKit 2 Requirement
**Writing Tools requires TextKit 2 for the full inline experience.** TextKit 1 views only get the limited panel-based experience (no inline proofreading marks).
```swift
// ✅ Gets full inline Writing Tools
let textView = UITextView(usingTextLayoutManager: true)
// ❌ Only gets panel-based Writing Tools
let textView = UITextView(usingTextLayoutManager: false)
```
**Check:** If Writing Tools appears only in a popover (no inline marks), verify the view is using TextKit 2.
## Delegate Methods (UITextView)
### Activity Notifications
```swift
func textViewWritingToolsWillBegin(_ textView: UITextView) {
// Pause operations that could conflict:
// - Undo coalescing
// - Syncing to server
// - Collaborative editing updates
}
func textViewWritingToolsDidEnd(_ textView: UITextView) {
// Resume normal operations
}
```
### Checking Active State
```swift
if textView.isWritingToolsActive {
// Writing Tools is running — don't modify text
} else {
// Safe to manipulate text
}
```
### Protected Ranges
Exclude ranges from Writing Tools rewriting (code blocks, quotes, citations):
```swift
func textView(_ textView: UITextView,
writingToolsIgnoredRangesIn enclosingRange: NSRange) -> [NSRange] {
// Return ranges that should NOT be rewritten
var protectedRanges: [NSRange] = []
// Find code blocks
let codePattern = try! NSRegularExpression(pattern: "```[\\s\\S]*?```")
let matches = codePattern.matches(in: textView.text, range: enclosingRange)
protectedRanges.append(contentsOf: matches.map(\.range))
return protectedRanges
}
```
### NSTextView Equivalents (macOS)
```swift
func textViewWritingToolsWillBegin(_ notification: Notification)
func textViewWritingToolsDidEnd(_ notification: Notification)
// Protected ranges
func textView(_ textView: NSTextView,
writingToolsIgnoredRangesIn range: NSRange) -> [NSRange]
```
## Custom Text View Integration (iOS 18)
For views using `UITextInput` (not UITextView), Writing Tools is available through the callout bar if the view adopts `UITextInteraction`:
```swift
class CustomTextView: UIView, UITextInput {
let textInteraction = UITextInteraction(for: .editable)
override init(frame: CGRect) {
super.init(frame: frame)
textInteraction.textInput = self
addInteraction(textInteraction)
// Writing Tools appears in callout bar automatically
}
}
```
**Alternative:** Adopt `UITextSelectionDisplayInteraction` + `UIEditMenuInteraction` separately.
## UIWritingToolsCoordinator (iOS 26+)
For fully custom text engines (not using UITextInput), the coordinator provides direct Writing Tools integration with animation support.
### Setup
```swift
class CustomEditorView: UIView {
var coordinator: UIWritingToolsCoordinator!
override init(frame: CGRect) {
super.init(frame: frame)
coordinator = UIWritingToolsCoordinator(delegate: self)
addInteraction(coordinator)
}
}
```
### Delegate Protocol
All methods are **async**:
```swift
extension CustomEditorView: UIWritingToolsCoordinatorDelegate {
// Provide text content for Writing Tools to process
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
requestsContextFor ranges: [UIWritingToolsCoordinator.TextRange]
) async -> UIWritingToolsCoordinator.Context {
let attributedString = getAttributedString(for: ranges)
return UIWritingToolsCoordinator.Context(
attributedString: attributedString,
range: NSRange(location: 0, length: attributedString.length)
)
}
// Handle text replacement
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
replaceRange range: UIWritingToolsCoordinator.TextRange,
with attributedString: NSAttributedString,
reason: UIWritingToolsCoordinator.TextReplacementReason
) async {
applyReplacement(attributedString, in: range)
}
// State changes
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
didChangeState newState: UIWritingToolsCoordinator.State
) {
switch newState {
case .idle:
resumeNormalEditing()
case .nonInteractive:
pauseEditing()
case .interactiveStreaming:
showStreamingUI()
@unknown default:
break
}
}
}
```
### Animation Support
The coordinator supports animated text transitions:
```swift
// Provide preview for animation (text snapshot before change)
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
requestsPreviewFor range: UIWritingToolsCoordinator.TextRange
) async -> UIWritingToolsCoordinator.TextPreview {
let rect = getRect(for: range)
return UIWritingToolsCoordinator.TextPreview(
textView: self,
rect: rect
)
}
// Provide proofreading mark paths (underline positions)
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
requestsUnderlinePathFor range: UIWritingToolsCoordinator.TextRange
) async -> UIBezierPath {
return getUnderlinePath(for: range)
}
```
### NSWritingToolsCoordinator (macOS 26+)
macOS equivalent with the same delegate pattern:
```swift
let coordinator = NSWritingToolsCoordinator(delegate: self)
view.addInteraction(coordinator)
```
## macOS Custom Views (Pre-Coordinator)
For macOS views without UITextInput-equivalent, implement `NSServicesMenuRequestor`:
```swift
// In NSView or NSViewController
override func validRequestor(forSendType sendType: NSPasteboard.PasteboardType?,
returnType: NSPasteboard.PasteboardType?) -> Any? {
if sendType == .string || sendType == .rtf {
return self
}
return super.validRequestor(forSendType: sendType, returnType: returnType)
}
func writeSelection(to pboard: NSPasteboard, types: [NSPasteboard.PasteboardType]) -> Bool {
pboard.writeObjects([selectedText as NSString])
return true
}
func readSelection(from pboard: NSPasteboard) -> Bool {
guard let string = pboard.string(forType: .string) else { return false }
replaceSelection(with: string)
return true
}
```
## PresentationIntent (iOS 26+)
Mark text structure for better Writing Tools understanding:
```swift
var str = AttributedString("My Document")
// Mark as heading
str.presentationIntent = .header(level: 1)
// Mark as code block (Writing Tools will protect this)
codeBlock.presentationIntent = .codeBlock(languageHint: "swift")
// Mark as block quote
quote.presentationIntent = .blockQuote
```
## Writing Tools Decision Tree
```
Is your view UITextView or NSTextView?
YES → Set writingToolsBehavior + delegate methods. Done.
NO → Does it conform to UITextInput?
YES → Add UITextInteraction. Writing Tools in callout bar.
NO → iOS 26+?
YES → Use UIWritingToolsCoordinator
NO → Not directly supported. Consider wrapping in UITextView.
```
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| Writing Tools not in menu | `writingToolsBehavior = .none` or Apple Intelligence not enabled | Set `.default`; user must enable Apple Intelligence in Settings |
| Only panel mode, no inline | TextKit 1 mode or fallback | Ensure TextKit 2; check for `layoutManager` access triggering fallback |
| Writing Tools rewrites code | No protected ranges | Implement `writingToolsIgnoredRangesIn` delegate |
| Inline marks not animating | Missing coordinator | Use UIWritingToolsCoordinator (iOS 26+) |
| Text corrupted after rewrite | Editing during active session | Check `isWritingToolsActive` before modifications |
## Common Pitfalls
1. **TextKit 1 fallback kills inline Writing Tools** — Any access to `layoutManager` triggers fallback. Use TextKit 2.
2. **Not pausing operations during Writing Tools** — Server syncs, undo coalescing, etc. can conflict. Use `willBegin`/`didEnd`.
3. **Editing text while Writing Tools is active** — Check `isWritingToolsActive` before programmatic text changes.
4. **Forgetting protected ranges** — Code blocks, quotes, and citations should be excluded from rewriting.
5. **Assuming Writing Tools is always available** — It requires Apple Intelligence enabled. Check availability gracefully.
## Related Skills
- Use `/skill apple-text-textkit2-ref` when Writing Tools behavior depends on TextKit 2 capabilities.
- Use `/skill apple-text-fallback-triggers` when inline Writing Tools drops into limited mode.
- Use `/skill apple-text-input-ref` for lower-level custom text input requirements.