Text Input Reference
Use when the user already knows the problem lives in the text input system and needs exact UITextInput, UIKeyInput, NSTextInputClient, marked-text, or selection-UI behavior. Reach for this when implementing or debugging custom text input plumbing, not high-level editor interactions alone.
Use when the user already knows the problem lives in the text input system and needs exact UITextInput, UIKeyInput, NSTextInputClient, marked-text, or selection-UI behavior. Reach for this when implementing or debugging custom text input plumbing, not high-level editor interactions alone.
Family: Editor Features And Interaction
Use this skill when the main question is how custom text input should satisfy UIKit or AppKit input contracts.
When to Use
Section titled “When to Use”- You are implementing
UIKeyInput,UITextInput, orNSTextInputClient. - You need marked-text, selection, or geometry rules for custom editors.
- The problem is lower-level input behavior, not general view selection.
Quick Decision
Section titled “Quick Decision”- Simple Latin-only input ->
UIKeyInput - Full IME, selection, autocorrection, or geometry ->
UITextInput - macOS custom input view ->
NSTextInputClient
Core Guidance
Section titled “Core Guidance”Protocol Hierarchy (UIKit)
Section titled “Protocol Hierarchy (UIKit)”UIResponder └── UIKeyInput (minimal: insert, delete, hasText) └── UITextInput (full: positions, ranges, marked text, geometry)UIKeyInput (Minimal Input)
Section titled “UIKeyInput (Minimal Input)”Three methods for basic text entry:
class SimpleInputView: UIView, UIKeyInput { var text = ""
var hasText: Bool { !text.isEmpty }
func insertText(_ text: String) { self.text += text setNeedsDisplay() }
func deleteBackward() { guard !text.isEmpty else { return } text.removeLast() setNeedsDisplay() }
override var canBecomeFirstResponder: Bool { true }}Sufficient for: Simple single-stage input (Latin keyboard). Does NOT support:
- CJK multistage input (marked text)
- Autocorrection / spell checking
- Selection / cursor positioning
- Copy/paste
UITextInput (Full Input)
Section titled “UITextInput (Full Input)”Required Custom Types
Section titled “Required Custom Types”// Custom positionclass MyTextPosition: UITextPosition { let offset: Int init(_ offset: Int) { self.offset = offset }}
// Custom rangeclass MyTextRange: UITextRange { let start: MyTextPosition let end: MyTextPosition
override var start: UITextPosition { _start } override var end: UITextPosition { _end } override var isEmpty: Bool { _start.offset == _end.offset }
init(start: Int, end: Int) { _start = MyTextPosition(start) _end = MyTextPosition(end) }}Required Protocol Methods (Grouped by Purpose)
Section titled “Required Protocol Methods (Grouped by Purpose)”Text Access
Section titled “Text Access”// Read text in rangefunc text(in range: UITextRange) -> String?
// Replace text (main edit entry point from system)func replace(_ range: UITextRange, withText text: String)Position Arithmetic
Section titled “Position Arithmetic”// Offset from a positionfunc position(from position: UITextPosition, offset: Int) -> UITextPosition?
// Offset in a directionfunc position(from position: UITextPosition, in direction: UITextLayoutDirection, offset: Int) -> UITextPosition?
// Distance between positionsfunc offset(from: UITextPosition, to: UITextPosition) -> Int
// Compare positionsfunc compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResultRange Creation
Section titled “Range Creation”func textRange(from: UITextPosition, to: UITextPosition) -> UITextRange?Selection
Section titled “Selection”// Current selection (cursor = zero-length range)var selectedTextRange: UITextRange? { get set }Marked Text (CJK/IME)
Section titled “Marked Text (CJK/IME)”Marked text is provisionally inserted text during multistage input. Visually distinct (underlined).
// Current marked text range (nil = no marked text)var markedTextRange: UITextRange? { get }
// Style dictionary for marked textvar markedTextStyle: [NSAttributedString.Key: Any]? { get set }
// Set marked text with internal selectionfunc setMarkedText(_ markedText: String?, selectedRange: NSRange)
// Commit marked textfunc unmarkText()Lifecycle:
- User starts CJK input →
setMarkedText("拼", selectedRange: NSRange(1, 0)) - User continues →
setMarkedText("拼音", selectedRange: NSRange(2, 0)) - User confirms →
unmarkText()(marked text becomes regular text) - User cancels →
setMarkedText(nil, selectedRange: NSRange(0, 0))
Geometry (For System UI)
Section titled “Geometry (For System UI)”// Rect for caret at positionfunc caretRect(for position: UITextPosition) -> CGRect
// Rect covering a text range (for selection highlight)func firstRect(for range: UITextRange) -> CGRect
// All selection rects for a range (multi-line)func selectionRects(for range: UITextRange) -> [UITextSelectionRect]Hit Testing
Section titled “Hit Testing”// Position nearest to pointfunc closestPosition(to point: CGPoint) -> UITextPosition?
// Position nearest to point within rangefunc closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition?
// Character range at pointfunc characterRange(at point: CGPoint) -> UITextRange?Document Bounds
Section titled “Document Bounds”var beginningOfDocument: UITextPosition { get }var endOfDocument: UITextPosition { get }Tokenizer
Section titled “Tokenizer”// For word/sentence/paragraph boundariesvar tokenizer: UITextInputTokenizer { get }// Default: return UITextInputStringTokenizer(textInput: self)Notifying the System of Changes
Section titled “Notifying the System of Changes”var inputDelegate: UITextInputDelegate? { get set }
// MUST call these when modifying text/selection externallyinputDelegate?.textWillChange(self)// ... modify text ...inputDelegate?.textDidChange(self)
inputDelegate?.selectionWillChange(self)// ... modify selection ...inputDelegate?.selectionDidChange(self)Failure to call these causes the input system to desync — autocorrect stops working, marked text corrupts, keyboard misbehaves.
NSTextInputClient (AppKit)
Section titled “NSTextInputClient (AppKit)”The macOS equivalent for custom text input.
Key Methods
Section titled “Key Methods”protocol NSTextInputClient { // Insert confirmed text func insertText(_ string: Any, replacementRange: NSRange)
// Set/update marked text func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange)
// Commit marked text func unmarkText()
// Query selection func selectedRange() -> NSRange
// Query marked text func markedRange() -> NSRange
// Check if has marked text func hasMarkedText() -> Bool
// Geometry func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect
// Hit testing func characterIndex(for point: NSPoint) -> Int
// Content access func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? func attributedString() -> NSAttributedString
// Valid attributes func validAttributesForMarkedText() -> [NSAttributedString.Key]}NSTextInputContext
Section titled “NSTextInputContext”Manages the input context (keyboard layout, input method) for a view:
// Get current input contextlet context = NSTextInputContext.current
// Invalidate character coordinates (after layout change)context?.invalidateCharacterCoordinates()
// Discard marked textcontext?.discardMarkedText()iOS Selection UI (iOS 17+)
Section titled “iOS Selection UI (iOS 17+)”UITextSelectionDisplayInteraction
Section titled “UITextSelectionDisplayInteraction”Provides system selection UI (cursor, handles, highlights) for custom text views:
class CustomTextView: UIView, UITextInput { lazy var selectionDisplay = UITextSelectionDisplayInteraction(textInput: self, isAccessibilityElement: false)
override init(frame: CGRect) { super.init(frame: frame) addInteraction(selectionDisplay) }
// Call when selection changes func updateSelection() { selectionDisplay.setNeedsSelectionUpdate() }}UITextLoupeSession
Section titled “UITextLoupeSession”Magnifier loupe for precise cursor positioning:
// Begin loupe at pointlet session = UITextLoupeSession.begin(at: point, from: selectionWidget, in: self)
// Move during dragsession.move(to: newPoint)
// Endsession.invalidate()macOS Cursor (macOS Sonoma+)
Section titled “macOS Cursor (macOS Sonoma+)”NSTextInsertionIndicator
Section titled “NSTextInsertionIndicator”System text cursor for custom AppKit views:
let indicator = NSTextInsertionIndicator(frame: .zero)documentView.addSubview(indicator)
// Required: set up effects view (for animation effects)indicator.effectsViewInserter = { effectView in self.documentView.addSubview(effectView, positioned: .above, relativeTo: nil)}
// Show/hideindicator.displayMode = .automatic // or .hiddenText Interactions
Section titled “Text Interactions”UITextInteraction (iOS 13+)
Section titled “UITextInteraction (iOS 13+)”Adds system text interactions to a view (selection gestures, menu):
let interaction = UITextInteraction(for: .editable) // or .nonEditableinteraction.textInput = self // Must conform to UITextInputview.addInteraction(interaction)UIEditMenuInteraction (iOS 16+)
Section titled “UIEditMenuInteraction (iOS 16+)”The modern replacement for UIMenuController:
let editMenu = UIEditMenuInteraction(delegate: self)view.addInteraction(editMenu)
// Show menulet config = UIEditMenuConfiguration(identifier: nil, sourcePoint: point)editMenu.presentEditMenu(with: config)Quick Reference
Section titled “Quick Reference”| Need | UIKit | AppKit |
|---|---|---|
| Minimal text input | UIKeyInput | NSTextInputClient |
| Full text input | UITextInput | NSTextInputClient |
| System cursor | UITextSelectionDisplayInteraction | NSTextInsertionIndicator |
| Magnifier | UITextLoupeSession | N/A (system-provided) |
| Selection gestures | UITextInteraction | N/A (NSTextView built-in) |
| Edit menu | UIEditMenuInteraction | NSMenu (right-click) |
| Input context | N/A (automatic) | NSTextInputContext |
Scribble / Apple Pencil Handwriting (iPadOS 14+)
Section titled “Scribble / Apple Pencil Handwriting (iPadOS 14+)”UITextView and UITextField get Scribble for free. Custom UITextInput views also get automatic support if the implementation is complete.
For customization (UIScribbleInteraction) and non-text-input views that should accept handwriting (UIIndirectScribbleInteraction), see scribble-patterns.md.
Common Pitfalls
Section titled “Common Pitfalls”- Not calling
inputDelegatemethods — System desyncs. Autocorrect and marked text break. - Wrong
caretRect/firstRect— System UI (keyboard, autocorrect, selection handles) positioned incorrectly. - Ignoring marked text — CJK input stops working. Always implement
setMarkedText/unmarkText. - Forgetting
canBecomeFirstResponder— View never receives keyboard input. - Using UITextInput without UITextSelectionDisplayInteraction — No cursor or selection handles shown.
- Not invalidating character coordinates (macOS) — After layout changes, call
NSTextInputContext.current?.invalidateCharacterCoordinates().
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-input-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-representable: Use when embedding UITextView or NSTextView inside SwiftUI and the hard part is wrapper behavior: two-way binding, focus, sizing, cursor preservation, update loops, toolbars, or environment bridging. Reach for this when native SwiftUI text views are not enough, not when choosing between text stacks at a high level.apple-text-writing-tools: Use when integrating Writing Tools into a native or custom text editor, configuring writingToolsBehavior, adopting UIWritingToolsCoordinator, protecting ranges, or debugging why Writing Tools do not appear. Reach for this when the problem is specifically Writing Tools, not generic editor debugging.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.
Sidecar Files
Section titled “Sidecar Files”skills/apple-text-input-ref/scribble-patterns.md
Full SKILL.md source
---name: apple-text-input-refdescription: Use when the user already knows the problem lives in the text input system and needs exact UITextInput, UIKeyInput, NSTextInputClient, marked-text, or selection-UI behavior. Reach for this when implementing or debugging custom text input plumbing, not high-level editor interactions alone.license: MIT---
# Text Input Reference
Use this skill when the main question is how custom text input should satisfy UIKit or AppKit input contracts.
## When to Use
- You are implementing `UIKeyInput`, `UITextInput`, or `NSTextInputClient`.- You need marked-text, selection, or geometry rules for custom editors.- The problem is lower-level input behavior, not general view selection.
## Quick Decision
- Simple Latin-only input -> `UIKeyInput`- Full IME, selection, autocorrection, or geometry -> `UITextInput`- macOS custom input view -> `NSTextInputClient`
## Core Guidance
## Protocol Hierarchy (UIKit)
```UIResponder └── UIKeyInput (minimal: insert, delete, hasText) └── UITextInput (full: positions, ranges, marked text, geometry)```
## UIKeyInput (Minimal Input)
Three methods for basic text entry:
```swiftclass SimpleInputView: UIView, UIKeyInput { var text = ""
var hasText: Bool { !text.isEmpty }
func insertText(_ text: String) { self.text += text setNeedsDisplay() }
func deleteBackward() { guard !text.isEmpty else { return } text.removeLast() setNeedsDisplay() }
override var canBecomeFirstResponder: Bool { true }}```
**Sufficient for:** Simple single-stage input (Latin keyboard). Does NOT support:- CJK multistage input (marked text)- Autocorrection / spell checking- Selection / cursor positioning- Copy/paste
## UITextInput (Full Input)
### Required Custom Types
```swift// Custom positionclass MyTextPosition: UITextPosition { let offset: Int init(_ offset: Int) { self.offset = offset }}
// Custom rangeclass MyTextRange: UITextRange { let start: MyTextPosition let end: MyTextPosition
override var start: UITextPosition { _start } override var end: UITextPosition { _end } override var isEmpty: Bool { _start.offset == _end.offset }
init(start: Int, end: Int) { _start = MyTextPosition(start) _end = MyTextPosition(end) }}```
### Required Protocol Methods (Grouped by Purpose)
#### Text Access
```swift// Read text in rangefunc text(in range: UITextRange) -> String?
// Replace text (main edit entry point from system)func replace(_ range: UITextRange, withText text: String)```
#### Position Arithmetic
```swift// Offset from a positionfunc position(from position: UITextPosition, offset: Int) -> UITextPosition?
// Offset in a directionfunc position(from position: UITextPosition, in direction: UITextLayoutDirection, offset: Int) -> UITextPosition?
// Distance between positionsfunc offset(from: UITextPosition, to: UITextPosition) -> Int
// Compare positionsfunc compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResult```
#### Range Creation
```swiftfunc textRange(from: UITextPosition, to: UITextPosition) -> UITextRange?```
#### Selection
```swift// Current selection (cursor = zero-length range)var selectedTextRange: UITextRange? { get set }```
#### Marked Text (CJK/IME)
Marked text is provisionally inserted text during multistage input. Visually distinct (underlined).
```swift// Current marked text range (nil = no marked text)var markedTextRange: UITextRange? { get }
// Style dictionary for marked textvar markedTextStyle: [NSAttributedString.Key: Any]? { get set }
// Set marked text with internal selectionfunc setMarkedText(_ markedText: String?, selectedRange: NSRange)
// Commit marked textfunc unmarkText()```
**Lifecycle:**1. User starts CJK input → `setMarkedText("拼", selectedRange: NSRange(1, 0))`2. User continues → `setMarkedText("拼音", selectedRange: NSRange(2, 0))`3. User confirms → `unmarkText()` (marked text becomes regular text)4. User cancels → `setMarkedText(nil, selectedRange: NSRange(0, 0))`
#### Geometry (For System UI)
```swift// Rect for caret at positionfunc caretRect(for position: UITextPosition) -> CGRect
// Rect covering a text range (for selection highlight)func firstRect(for range: UITextRange) -> CGRect
// All selection rects for a range (multi-line)func selectionRects(for range: UITextRange) -> [UITextSelectionRect]```
#### Hit Testing
```swift// Position nearest to pointfunc closestPosition(to point: CGPoint) -> UITextPosition?
// Position nearest to point within rangefunc closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition?
// Character range at pointfunc characterRange(at point: CGPoint) -> UITextRange?```
#### Document Bounds
```swiftvar beginningOfDocument: UITextPosition { get }var endOfDocument: UITextPosition { get }```
#### Tokenizer
```swift// For word/sentence/paragraph boundariesvar tokenizer: UITextInputTokenizer { get }// Default: return UITextInputStringTokenizer(textInput: self)```
### Notifying the System of Changes
```swiftvar inputDelegate: UITextInputDelegate? { get set }
// MUST call these when modifying text/selection externallyinputDelegate?.textWillChange(self)// ... modify text ...inputDelegate?.textDidChange(self)
inputDelegate?.selectionWillChange(self)// ... modify selection ...inputDelegate?.selectionDidChange(self)```
**Failure to call these** causes the input system to desync — autocorrect stops working, marked text corrupts, keyboard misbehaves.
## NSTextInputClient (AppKit)
The macOS equivalent for custom text input.
### Key Methods
```swiftprotocol NSTextInputClient { // Insert confirmed text func insertText(_ string: Any, replacementRange: NSRange)
// Set/update marked text func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange)
// Commit marked text func unmarkText()
// Query selection func selectedRange() -> NSRange
// Query marked text func markedRange() -> NSRange
// Check if has marked text func hasMarkedText() -> Bool
// Geometry func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect
// Hit testing func characterIndex(for point: NSPoint) -> Int
// Content access func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? func attributedString() -> NSAttributedString
// Valid attributes func validAttributesForMarkedText() -> [NSAttributedString.Key]}```
### NSTextInputContext
Manages the input context (keyboard layout, input method) for a view:
```swift// Get current input contextlet context = NSTextInputContext.current
// Invalidate character coordinates (after layout change)context?.invalidateCharacterCoordinates()
// Discard marked textcontext?.discardMarkedText()```
## iOS Selection UI (iOS 17+)
### UITextSelectionDisplayInteraction
Provides system selection UI (cursor, handles, highlights) for custom text views:
```swiftclass CustomTextView: UIView, UITextInput { lazy var selectionDisplay = UITextSelectionDisplayInteraction(textInput: self, isAccessibilityElement: false)
override init(frame: CGRect) { super.init(frame: frame) addInteraction(selectionDisplay) }
// Call when selection changes func updateSelection() { selectionDisplay.setNeedsSelectionUpdate() }}```
### UITextLoupeSession
Magnifier loupe for precise cursor positioning:
```swift// Begin loupe at pointlet session = UITextLoupeSession.begin(at: point, from: selectionWidget, in: self)
// Move during dragsession.move(to: newPoint)
// Endsession.invalidate()```
## macOS Cursor (macOS Sonoma+)
### NSTextInsertionIndicator
System text cursor for custom AppKit views:
```swiftlet indicator = NSTextInsertionIndicator(frame: .zero)documentView.addSubview(indicator)
// Required: set up effects view (for animation effects)indicator.effectsViewInserter = { effectView in self.documentView.addSubview(effectView, positioned: .above, relativeTo: nil)}
// Show/hideindicator.displayMode = .automatic // or .hidden```
## Text Interactions
### UITextInteraction (iOS 13+)
Adds system text interactions to a view (selection gestures, menu):
```swiftlet interaction = UITextInteraction(for: .editable) // or .nonEditableinteraction.textInput = self // Must conform to UITextInputview.addInteraction(interaction)```
### UIEditMenuInteraction (iOS 16+)
The modern replacement for UIMenuController:
```swiftlet editMenu = UIEditMenuInteraction(delegate: self)view.addInteraction(editMenu)
// Show menulet config = UIEditMenuConfiguration(identifier: nil, sourcePoint: point)editMenu.presentEditMenu(with: config)```
## Quick Reference
| Need | UIKit | AppKit ||------|-------|--------|| Minimal text input | UIKeyInput | NSTextInputClient || Full text input | UITextInput | NSTextInputClient || System cursor | UITextSelectionDisplayInteraction | NSTextInsertionIndicator || Magnifier | UITextLoupeSession | N/A (system-provided) || Selection gestures | UITextInteraction | N/A (NSTextView built-in) || Edit menu | UIEditMenuInteraction | NSMenu (right-click) || Input context | N/A (automatic) | NSTextInputContext |
## Scribble / Apple Pencil Handwriting (iPadOS 14+)
UITextView and UITextField get Scribble for free. Custom `UITextInput` views also get automatic support if the implementation is complete.
For customization (`UIScribbleInteraction`) and non-text-input views that should accept handwriting (`UIIndirectScribbleInteraction`), see [scribble-patterns.md](scribble-patterns.md).
## Common Pitfalls
1. **Not calling `inputDelegate` methods** — System desyncs. Autocorrect and marked text break.2. **Wrong `caretRect` / `firstRect`** — System UI (keyboard, autocorrect, selection handles) positioned incorrectly.3. **Ignoring marked text** — CJK input stops working. Always implement `setMarkedText`/`unmarkText`.4. **Forgetting `canBecomeFirstResponder`** — View never receives keyboard input.5. **Using UITextInput without UITextSelectionDisplayInteraction** — No cursor or selection handles shown.6. **Not invalidating character coordinates (macOS)** — After layout changes, call `NSTextInputContext.current?.invalidateCharacterCoordinates()`.
## Related Skills
- Use `/skill apple-text-representable` when custom input lives inside a SwiftUI wrapper.- Use `/skill apple-text-writing-tools` when text input requirements intersect with Apple Intelligence editing.- Use `/skill apple-text-textkit-diag` when the problem is symptom-first rather than protocol-first.