Text Input Reference
Use when implementing or debugging UITextInput, UIKeyInput, or NSTextInputClient — marked text, selection UI, custom input.
Use when implementing or debugging UITextInput, UIKeyInput, or NSTextInputClient — marked text, selection UI, custom input.
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.
CJK / IME Gotchas
Section titled “CJK / IME Gotchas”These are common bugs specific to multistage input (Chinese, Japanese, Korean). They do not affect Latin-only input.
-
UIKeyInput alone cannot support CJK. Multi-stage input methods require
setMarkedText/unmarkText. If your view only adoptsUIKeyInput, CJK keyboards will not work. -
Two-way binding corruption with reactive frameworks. Reactive bindings (RxSwift, Combine, SwiftUI
@Binding) that read and write text on every change corrupt CJK composition. Japanese input produces garbled output. Fix: suppress text observation callbacks whilemarkedTextRange != nil.func textDidChange() {guard markedTextRange == nil else { return } // Skip during compositionbinding.wrappedValue = text} -
selectedRangeinsetMarkedTextis relative to marked text, not the document. AddmarkedTextRange.locationto get the document-relative cursor position. -
setMarkedTextcan fire twice for a single composition event on iOS. The first call may report an inconsistent state. Guard against re-entrant processing. -
deleteBackward()counts grapheme clusters, butadjustTextPosition(byCharacterOffset:)counts UTF-16 code units. Mixing these in CJK text (where one character can be multiple UTF-16 units) produces off-by-one cursor jumps. -
NSTextInputClient (macOS) has an extra
replacementRangeparameter insetMarkedText(_:selectedRange:replacementRange:). Some Japanese IMEs use this for reconverting committed text. If you passNSRange(location: NSNotFound, length: 0), the system uses the current selection.
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 wrapping UITextView or NSTextView in SwiftUI — binding, focus, sizing, cursor preservation, or update loops.apple-text-writing-tools: Use when integrating Writing Tools — writingToolsBehavior, UIWritingToolsCoordinator, protected ranges, or inline vs panel mode.apple-text-textkit-diag: Use when debugging broken text — stale layout, editing crashes, fallback, Writing Tools issues, or rendering artifacts.
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 implementing or debugging UITextInput, UIKeyInput, or NSTextInputClient — marked text, selection UI, custom inputlicense: 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).
## CJK / IME Gotchas
These are common bugs specific to multistage input (Chinese, Japanese, Korean). They do not affect Latin-only input.
1. **UIKeyInput alone cannot support CJK.** Multi-stage input methods require `setMarkedText`/`unmarkText`. If your view only adopts `UIKeyInput`, CJK keyboards will not work.
2. **Two-way binding corruption with reactive frameworks.** Reactive bindings (RxSwift, Combine, SwiftUI `@Binding`) that read and write text on every change corrupt CJK composition. Japanese input produces garbled output. **Fix:** suppress text observation callbacks while `markedTextRange != nil`.
```swift func textDidChange() { guard markedTextRange == nil else { return } // Skip during composition binding.wrappedValue = text } ```
3. **`selectedRange` in `setMarkedText` is relative to marked text, not the document.** Add `markedTextRange.location` to get the document-relative cursor position.
4. **`setMarkedText` can fire twice for a single composition event on iOS.** The first call may report an inconsistent state. Guard against re-entrant processing.
5. **`deleteBackward()` counts grapheme clusters, but `adjustTextPosition(byCharacterOffset:)` counts UTF-16 code units.** Mixing these in CJK text (where one character can be multiple UTF-16 units) produces off-by-one cursor jumps.
6. **NSTextInputClient (macOS) has an extra `replacementRange` parameter** in `setMarkedText(_:selectedRange:replacementRange:)`. Some Japanese IMEs use this for reconverting committed text. If you pass `NSRange(location: NSNotFound, length: 0)`, the system uses the current selection.
## 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.