Patterns for power users — client-side options, re-edit behavior, LLM-identified params, error handling, 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, 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.
Additional context
Suggestions can be conditioned on what your app already knows — a user profile, workspace state, the thing being edited. Pass it as free-form JSON (JSONValue) and it rides every request as additional_context; the server steers suggested parameters and option values toward it but never interprets or stores it.
// Seed it once at construction — a user profile the whole session shares.var config = AIAutocompleteController.Configuration(apiConfig: .apiKey(.init(apiKey: "pk_v1_your_public_key")))config.additionalContext = .object(["tier": .string("gold"),"home_store": .string("Gangnam Station"),"allergies": .array([.string("peanut")]),"preferences": .object(["milk": .string("oat milk"), "size": .string("grande")]),"favorites": .array([.string("iced americano"), .string("cold brew")])])// Or update it as the session evolves — the next request simply carries// the new value. Assigning never fires a request of its own, and the// value survives reset().controller.additionalContext = .object(["recent_orders": .array(recentOrders.suffix(10).map { order in.object(["drink": .string(order.drink), "size": .string(order.size)])})])
- Keys that match your product's catalog field names are honoured most reliably; a value named for a field you suggest is listed first.
- The server measures the compacted UTF-8 payload against a 2000-byte cap (AutocompleteRequest.maxAdditionalContextBytes); anything past it is truncated with a trailing "…" rather than rejected, so the tail is lost without an error — check JSONValue.compactUTF8ByteCount in debug builds and keep a rolling window rather than appending forever.
- It is sent only to the suggest endpoint, never to telemetry, and is treated strictly as data — but it is not subject to maskCompletedText, so keep PII out unless you mean to send it.
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.