Patterns for power users — client-side options, re-edit behavior, LLM-identified params, error handling, and accessibility.
Inject field options at runtime
Fields whose options come from your own data (a location search, a customer list, live inventory) have their own guide. Field Injection walks through marking a field as injected in Edit Logic and wiring optionOverrides to your endpoint.
Personalize suggestions per user
Suggestions can be tailored to each user with what your app already knows about them: a profile, saved preferences, recent activity. Personalization covers what to send, how to keep it current, the size cap, and what stays private.
Re-editing completed params
Filled pills stay editable — the full views handle all of this for you:
- Tapping a completed param (or moving the caret inside it) enters re-edit mode and reopens its cached options — no refetch.
- Picking a different option replaces the param's text atomically, then refetches suggestions — everything suggested after that param was conditioned on the value you just replaced.
- Deleting through a pill's edge removes the whole param; removeLastParam() does the same programmatically.
LLM-identified params
The server can recognize a span the user typed themselves as a parameter — no option tap involved. The full views render these as chips automatically; headless consumers read them from controller.identifiedParams and the .identified segment case.
- Identified chips are tentative: each response replaces the set, and a chip disappears as soon as its text is edited away or a completed param claims the span.
- They are not re-editable — there are no cached options behind them — so Backspace at a chip's trailing edge deletes it whole in one press. Custom inputs get the same behavior from removeIdentifiedParam(atCaret:).
- They're echoed to the server on the next request so it keeps resolving the same span, but they are never placeholder-substituted into rawQuery — only completed params are.
Error handling
Every terminal failure reaches onError as a typed AIAutocompleteError — match on the cases you care about.
AIAutocompleteController.Configuration(apiConfig: .apiKey(.init(apiKey: "pk_v1_your_public_key")),onError: { error inswitch error {case .network(let urlError):print("Offline or timed out: \(urlError)")case .http(let status, let message):print("Server returned \(status): \(message ?? "")")case .unauthorized:print("Token rejected twice — re-authenticate")case .tokenProvider(let underlying):print("getAccessToken threw: \(underlying)")default:print("Unexpected: \(error)")}})
Cancelled fetches (superseded by newer keystrokes) are consumed internally and never reach onError. The controller also mirrors the last error on its error property — clear it with dismissError().
Accessibility
The views are built on UIKit text and collection primitives and respect system accessibility settings:
- Dynamic Type: all SDK text scales with the user's setting; option text is clamped at maximumContentSizeCategory.
- Reduce Motion: always wins over the animations token — visual effects stop while the rest of the UI behaves identically.
- Haptics: the promotion tick respects the haptics switch and is independent of animations, so Reduce Motion users keep the tactile cue.