Skip to content

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.

Reference Skills

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.

  • 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.
  • Simple Latin-only input -> UIKeyInput
  • Full IME, selection, autocorrection, or geometry -> UITextInput
  • macOS custom input view -> NSTextInputClient
UIResponder
└── UIKeyInput (minimal: insert, delete, hasText)
└── UITextInput (full: positions, ranges, marked text, geometry)

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
// Custom position
class MyTextPosition: UITextPosition {
let offset: Int
init(_ offset: Int) { self.offset = offset }
}
// Custom range
class 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)”
// Read text in range
func text(in range: UITextRange) -> String?
// Replace text (main edit entry point from system)
func replace(_ range: UITextRange, withText text: String)
// Offset from a position
func position(from position: UITextPosition, offset: Int) -> UITextPosition?
// Offset in a direction
func position(from position: UITextPosition, in direction: UITextLayoutDirection,
offset: Int) -> UITextPosition?
// Distance between positions
func offset(from: UITextPosition, to: UITextPosition) -> Int
// Compare positions
func compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResult
func textRange(from: UITextPosition, to: UITextPosition) -> UITextRange?
// Current selection (cursor = zero-length range)
var selectedTextRange: UITextRange? { get set }

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 text
var markedTextStyle: [NSAttributedString.Key: Any]? { get set }
// Set marked text with internal selection
func setMarkedText(_ markedText: String?, selectedRange: NSRange)
// Commit marked text
func 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))
// Rect for caret at position
func 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]
// Position nearest to point
func closestPosition(to point: CGPoint) -> UITextPosition?
// Position nearest to point within range
func closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition?
// Character range at point
func characterRange(at point: CGPoint) -> UITextRange?
var beginningOfDocument: UITextPosition { get }
var endOfDocument: UITextPosition { get }
// For word/sentence/paragraph boundaries
var tokenizer: UITextInputTokenizer { get }
// Default: return UITextInputStringTokenizer(textInput: self)
var inputDelegate: UITextInputDelegate? { get set }
// MUST call these when modifying text/selection externally
inputDelegate?.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.

The macOS equivalent for custom text input.

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]
}

Manages the input context (keyboard layout, input method) for a view:

// Get current input context
let context = NSTextInputContext.current
// Invalidate character coordinates (after layout change)
context?.invalidateCharacterCoordinates()
// Discard marked text
context?.discardMarkedText()

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()
}
}

Magnifier loupe for precise cursor positioning:

// Begin loupe at point
let session = UITextLoupeSession.begin(at: point, from: selectionWidget, in: self)
// Move during drag
session.move(to: newPoint)
// End
session.invalidate()

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/hide
indicator.displayMode = .automatic // or .hidden

Adds system text interactions to a view (selection gestures, menu):

let interaction = UITextInteraction(for: .editable) // or .nonEditable
interaction.textInput = self // Must conform to UITextInput
view.addInteraction(interaction)

The modern replacement for UIMenuController:

let editMenu = UIEditMenuInteraction(delegate: self)
view.addInteraction(editMenu)
// Show menu
let config = UIEditMenuConfiguration(identifier: nil, sourcePoint: point)
editMenu.presentEditMenu(with: config)
NeedUIKitAppKit
Minimal text inputUIKeyInputNSTextInputClient
Full text inputUITextInputNSTextInputClient
System cursorUITextSelectionDisplayInteractionNSTextInsertionIndicator
MagnifierUITextLoupeSessionN/A (system-provided)
Selection gesturesUITextInteractionN/A (NSTextView built-in)
Edit menuUIEditMenuInteractionNSMenu (right-click)
Input contextN/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.

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

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.

  • 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.
  • skills/apple-text-input-ref/scribble-patterns.md
Full SKILL.md source
SKILL.md
---
name: apple-text-input-ref
description: 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:
```swift
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)
### Required Custom Types
```swift
// Custom position
class MyTextPosition: UITextPosition {
let offset: Int
init(_ offset: Int) { self.offset = offset }
}
// Custom range
class 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 range
func 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 position
func position(from position: UITextPosition, offset: Int) -> UITextPosition?
// Offset in a direction
func position(from position: UITextPosition, in direction: UITextLayoutDirection,
offset: Int) -> UITextPosition?
// Distance between positions
func offset(from: UITextPosition, to: UITextPosition) -> Int
// Compare positions
func compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResult
```
#### Range Creation
```swift
func 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 text
var markedTextStyle: [NSAttributedString.Key: Any]? { get set }
// Set marked text with internal selection
func setMarkedText(_ markedText: String?, selectedRange: NSRange)
// Commit marked text
func 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 position
func 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 point
func closestPosition(to point: CGPoint) -> UITextPosition?
// Position nearest to point within range
func closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition?
// Character range at point
func characterRange(at point: CGPoint) -> UITextRange?
```
#### Document Bounds
```swift
var beginningOfDocument: UITextPosition { get }
var endOfDocument: UITextPosition { get }
```
#### Tokenizer
```swift
// For word/sentence/paragraph boundaries
var tokenizer: UITextInputTokenizer { get }
// Default: return UITextInputStringTokenizer(textInput: self)
```
### Notifying the System of Changes
```swift
var inputDelegate: UITextInputDelegate? { get set }
// MUST call these when modifying text/selection externally
inputDelegate?.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
```swift
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
Manages the input context (keyboard layout, input method) for a view:
```swift
// Get current input context
let context = NSTextInputContext.current
// Invalidate character coordinates (after layout change)
context?.invalidateCharacterCoordinates()
// Discard marked text
context?.discardMarkedText()
```
## iOS Selection UI (iOS 17+)
### UITextSelectionDisplayInteraction
Provides system selection UI (cursor, handles, highlights) for custom text views:
```swift
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
Magnifier loupe for precise cursor positioning:
```swift
// Begin loupe at point
let session = UITextLoupeSession.begin(at: point, from: selectionWidget, in: self)
// Move during drag
session.move(to: newPoint)
// End
session.invalidate()
```
## macOS Cursor (macOS Sonoma+)
### NSTextInsertionIndicator
System text cursor for custom AppKit views:
```swift
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/hide
indicator.displayMode = .automatic // or .hidden
```
## Text Interactions
### UITextInteraction (iOS 13+)
Adds system text interactions to a view (selection gestures, menu):
```swift
let interaction = UITextInteraction(for: .editable) // or .nonEditable
interaction.textInput = self // Must conform to UITextInput
view.addInteraction(interaction)
```
### UIEditMenuInteraction (iOS 16+)
The modern replacement for UIMenuController:
```swift
let editMenu = UIEditMenuInteraction(delegate: self)
view.addInteraction(editMenu)
// Show menu
let 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.