| name | text-editing |
| description | Build and customize interactive text editors on iOS / iPadOS / macOS (iOS 26 / Swift 6 / Xcode 26) — UITextInput / NSTextInputClient, marked text / IME, UITextView / NSTextView seams, spell-check / autocorrect, copy / paste, Writing Tools, NSTextStorage, find & replace, undo / redo, syntax highlighting, Core Text, Markdown, AttributedString + SwiftUI TextEditor, layout / attachments, and the TextKit 2 → 1 fallback. Use when building a code/text editor, on "cursor jumps", "CJK / IME", "paste brings in fonts", "Writing Tools went panel-only", "undo undid too much", "rich text formatting lost", "textLayoutManager is nil". Not for the NSAttributedString.Key catalog or text drag-and-drop. |
| license | MIT |
Interactive Text Editing & Input
Authored against iOS 26.x / iPadOS 26.x / macOS 26.x / Swift 6.x / Xcode 26.x.
This skill is for building text editors and customizing the editing experience on Apple platforms: the input contract a custom view owes the system, the customization seams stock views expose (selection, menus, links, cursor), spell/autocorrect wiring, the pasteboard, Writing Tools, and the TextKit fallback that silently breaks all of it. The unifying fact is that a real editor has to satisfy many contracts simultaneously — input, geometry, selection, IME, undo — before the system stops misbehaving; getting one piece wrong produces a symptom that looks unrelated to its cause.
The patterns here are clues, not answers. Before quoting any specific API signature, delegate name, or behavior, open the actual editing code and verify the call site matches, then confirm the signature against the current Apple docs via Sosumi (sosumi.ai/documentation/<framework>/<api> — Apple's developer.apple.com pages are JS-rendered and return a stub to non-browser fetches). The input, IME, and Writing Tools surfaces shift across iOS releases, and a memorized signature is the most common source of confidently-wrong editor code. The grounding workflow and the curated Sosumi link set live in references/grounding.md.
Each topic below keeps only its headline facts; the full treatment (code, tables, traps) lives in the linked reference file.
The first decision: do you need a custom input view?
Most editing work does not need a custom UITextInput view — UITextView/NSTextView already implement the whole input contract, IME, selection UI, menus, spell-check, paste, and Writing Tools; subclass one and override the seams. Go fully custom only when there is genuinely no text view to lean on (canvas-rendered code editor, game UI): the protocol is several hundred lines that must all be right at once, plus a private-API spell-correction trap.
TextKit 2 → 1 fallback: what silently breaks editors
Any access to the legacy layout manager — even a read-only if textView.layoutManager != nil — permanently flips that view instance to TextKit 1: Writing Tools degrade to panel-only, NSTextAttachmentViewProvider attachments vanish, custom cursor drawing stops; third-party gutters/highlighters trigger it for you, and one macOS field editor flips every NSTextField in the window. Read textView.textLayoutManager (nil-safe) instead; opt in to TextKit 1 deliberately with UITextView(usingTextLayoutManager: false).
UITextInput: the custom-editor contract
A custom editor adopts UITextInput (UIKeyInput alone can't satisfy CJK, Scribble, or autocorrect), returns concrete UITextPosition/UITextRange subclasses, does ALL position arithmetic in UTF-16, adds UITextInteraction + UITextSelectionDisplayInteraction for gestures and selection UI, and brackets every external mutation with inputDelegate will/did calls — or autocorrect silently dies.
Marked text and IME composition
Marked text is the IME's provisional string. The selectedRange in setMarkedText(_:selectedRange:) is relative to the marked text, not the document; and echoing text back into the view while markedTextRange != nil destroys composition — the root cause of the "cursor jumps to the end" SwiftUI-wrapper bug. Gate every outbound binding write on markedTextRange == nil.
Selection, edit menus, links, gestures, and cursor on stock views
UIEditMenuInteraction (append actions by identifier, never index), UITextItem .uiTextItemTag for tappable non-link text without link styling, menu(for:) + validateMenuItem on macOS, system-gesture coordination, pushing selection out of protected ranges in textViewDidChangeSelection, and the shared tintColor cursor/link-color trap (caretRect(for:) → .zero to hide the cursor).
Spell check and autocorrect
Stock views configure via UITextInputTraits (iOS — trait changes require the view to NOT be first responder) or per-feature Bools (macOS); a code editor disables every rewriting trait (spell, autocorrect, autocapitalization, smart quotes/dashes, prediction). Custom UITextInput views hit the private-API correction trap: underlines and the popover render, but applying a suggestion dispatches through private UITextReplacement — build your own UI on UITextChecker instead, or use UITextView.
Copy, cut, and paste
Intercept paste by overriding paste(_:) — textView(_:shouldChangeTextIn:replacementText:) sees plaintext only, even on rich pastes. A programmatic paste must apply typingAttributes to the inserted range and move the caret itself. NSItemProvider completion handlers run on arbitrary threads and must check the highest-fidelity type first, plaintext last.
- Full pasteboard guide (sanitization, multi-representation copy, iOS 14+ paste prompt, named pasteboards): references/pasteboard.md
Find and replace
Stock UITextView: isFindInteractionEnabled = true + presentFindNavigator(showingReplace:). Custom views adopt UITextSearching; macOS uses usesFindBar / NSTextFinderClient. The three traps: a missing finishedSearching() spins the find bar forever; decoration style .normal means remove decoration; replaceAll must iterate ranges.reversed().
Undo and redo
Stock views register undo automatically by observing NSTextStorage.processEditing; setting textView.text/attributedText replaces the storage and clears the undo stack. Wrap replace-all in one beginUndoGrouping(); compute inverses in UTF-16; always defer { enableUndoRegistration() }; guard custom registration with !textView.isWritingToolsActive.
Writing Tools / Apple Intelligence
Automatic on stock views when Apple Intelligence is enabled — absence is usually device state, not code. .table in UITextView.allowedWritingToolsResultOptions raises NSInvalidArgumentException; protected ranges return [NSValue], not [NSRange] (the wrong type is silently ignored); never mutate storage while isWritingToolsActive. Custom engines integrate via UIWritingToolsCoordinator (nested UIWritingToolsCoordinator.Delegate, completion-handler-based).
The text storage layer: NSTextStorage and the edit lifecycle
One model backs both TextKit stacks, so this layer survives fallback. Character mutations belong in willProcessEditing; didProcessEditing is attributes-only (mutating characters there is the classic re-entrancy crash). Batch with beginEditing/endEditing; a custom storage subclass must call edited(_:range:changeInLength:) or layout never updates.
Syntax highlighting and parsing edited text
Choose NSRegularExpression (NSRange-native, every OS) vs Swift Regex (typed captures, needs NSRange(_, in:) bridging) by deployment target and output type; hoist compilation out of the keystroke path; scope passes to the edited paragraph; visual-only highlighting uses temporary (TK1) or rendering (TK2) attributes, never storage. UITextView.dataDetectorTypes is ignored while editable — run NSDataDetector yourself. Production code highlighting is tree-sitter with incremental reparse.
Rich text: the value-type AttributedString model and SwiftUI TextEditor
On iOS 26, TextEditor(text: $attr, selection: $sel) bound to an AttributedString is a real rich-text editor — the canonical "rich text formatting lost" bug is binding a String. Toolbars use transformAttributes(in: &selection) and font.resolve(in: fontResolutionContext). Hard gaps (attachments, lists/tables, exclusion paths, highlighting, inputAccessoryView) still require wrapping a UITextView.
Markdown
AttributedString(markdown:) parses block structure only into presentationIntent runs — neither SwiftUI Text nor TextKit renders them; walk the runs and translate to NSParagraphStyle yourself. Use .inlineOnlyPreservingWhitespace for user-typed content.
Direct Core Text
Drop below TextKit only for glyph-level access, custom typesetting/line-breaking, or drawing into a CGContext you own. The perennial bugs: the bottom-left coordinate flip (reset textMatrix, flip y) and iOS non-bridging (kCTForegroundColorAttributeName wants CGColor — a UIColor renders default black).
Laying out edited text: wrapping, measurement, attachments, shapes
NSParagraphStyle traps (truncation acts on the last line only, lineSpacing vs paragraphSpacing, min+max line height to lock it); boundingRect needs .usesLineFragmentOrigin + .usesFontLeading + ceil(); NSTextAttachment.bounds y-origin is the baseline with positive y up; exclusion paths live in container coordinates. Multi-container layout and NSTextTable are TextKit 1 only.
Editing on macOS: NSTextInputClient and AppKit differences
AppKit's analog of UITextInput: NSRange-based methods, an extra replacementRange in setMarkedText, invalidateCharacterCoordinates() after layout changes, NSTextInsertionIndicator for the cursor. Other AppKit deltas (field-editor cascade, validateMenuItem, NSSpellChecker, named pasteboards, .table in Writing Tools) live in their topic files.
String / NSString / NSRange — the unit that breaks everything
Swift String counts graphemes; NSString/NSRange/TextKit/UITextInput count UTF-16 — they diverge on every emoji. Never build an NSRange from String.count; bridge with NSRange(_, in:) / Range(_, in:) and standardize on UTF-16 at the storage boundary.
Common Mistakes
The 24 highest-frequency text-editing bugs — fallback-triggering .layoutManager reads, IME echo, UTF-16 mismatches, paste/Writing-Tools/undo traps, the dark-mode-invisible bare NSAttributedString — each with its fix.
References
Bundled reference files (loaded only when this skill points to them):
- references/custom-input-view.md —
UITextInput contract + geometry, marked text/IME, NSTextInputClient (macOS), the iOS 18 content-collector crash.
- references/textkit-fallback.md — fallback trigger catalog, detection, opt-out/recovery, the iOS 16 scroll-trail bug.
- references/stock-view-customization.md — edit menus,
UITextItem, macOS context menus, gestures, selection constraints, cursor.
- references/spell-autocorrect.md — traits, the correction trap,
UITextChecker / NSSpellChecker, capability table.
- references/pasteboard.md — paste interception,
NSItemProvider, multi-representation copy, paste prompt.
- references/writing-tools.md — behaviors, protected ranges, lifecycle, coordinator signatures. Refresh against Sosumi after each Xcode 26.x point release.
- references/text-storage.md — edit lifecycle, delegate hooks, batching, subclassing, attribute performance.
- references/highlighting-and-parsing.md — regex engines, bridging, keystroke hygiene,
NSDataDetector / NLTagger / NLEmbedding, tree-sitter.
- references/layout-of-edited-text.md — paragraph styles, measurement, attachments, exclusion paths,
NSTextTable.
- references/find-and-replace.md —
UIFindInteraction, UITextSearching, NSTextFinder, viewport highlighting.
- references/undo-redo.md — grouping/coalescing, UTF-16
changeInLength, standalone storage, TK2 transactions.
- references/core-text.md —
CTLine/CTRun/CTTypesetter/CTRunDelegate, coordinate flip, non-bridging traps.
- references/markdown.md —
interpretedSyntax, PresentationIntent, the TextKit translator, custom ^[…] attributes.
- references/attributed-string-richtext.md — iOS-26
AttributedString model, SwiftUI TextEditor, AttributeScope, persistence.
- references/string-indexing-units.md — grapheme vs UTF-16 units,
NSRange bridging.
- references/common-mistakes.md — the 24 highest-frequency bugs with fixes.
- references/editor-recipes.md — line-number gutter, auto-pair/auto-indent, keyboard avoidance, per-keystroke perf traps,
.kern vs .tracking.
- references/grounding.md — verifying signatures via Sosumi +
xcrun mcpbridge, plus the curated Sosumi link set for every API this skill touches.
Adjacent sibling skills (this skill stays out of their lane): uikit / swiftui-ui (view-level work; the UIViewRepresentable wrapper hosting a UITextView), localization-rtl (bidi/RTL cursor behavior, writing direction), ios-performance (large-document performance beyond the TextKit-stack choice). Deliberately out of scope: the exhaustive NSAttributedString.Key value-and-compatibility catalog, and text drag-and-drop (UITextDraggable/UITextDroppable, NSDraggingSource).