apple-text-writing-tools

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

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

// 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

// 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).

// ✅ 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

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

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):

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)

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:

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

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:

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:

// 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:

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

macOS Custom Views (Pre-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
}

PresentationIntent (iOS 26+)

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

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

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

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.