Patterns for power users — client-side options, re-edit behavior, error handling, privacy, and accessibility.
Inject field options at runtime
Supply a field's option list from the app at runtime — on-device data, or options computed from what the user typed. Overrides are keyed per field and fully replace the server's options for that field. The closure receives the text typed against that pill.
- First, make sure the field exists as a runtime-injected custom field: in Edit Logic, answer Yes to the custom data fields question, add the field's name (e.g. Product Color), and pick "I will inject the values at runtime" as its type. Save so your product regenerates with the new field.
- Pass optionOverrides keyed by the field's name exactly as you typed it in Edit Logic — no renaming or case conversion. A field saved as pizza_type is keyed pizza_type; one saved as "pizza type" is keyed "pizza type". Whenever that field's pill becomes active, your function is called with the text the user has typed against the pill and returns the options to show.
- The returned list replaces the server's options for that field entirely — return every option you want visible, and an empty array when nothing matches.
// "Product Color" added as a runtime-injected custom field in Edit Logic.// The key must match the field name exactly as typed there.AIAutocompleteController.Configuration(apiConfig: .apiKey(.init(apiKey: "pk_v1_your_public_key")),optionOverrides: ["Product Color": { query incolors // your live data.filter { query.isEmpty || $0.localizedCaseInsensitiveContains(query) }.map { SuggestionOption(text: $0, isTappable: true) }},"amount": { query inlet digits = query.filter(\.isNumber)let amount = digits.isEmpty ? "$100" : "$\(digits)"return [SuggestionOption(text: amount, isTappable: true)]}])
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.
- Deleting through a pill's edge removes the whole param; removeLastParam() does the same programmatically.
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.