A small, self-contained SwiftUI demo where an on-device model assembles the interface at runtime. The model does not write SwiftUI. It produces a typed data structure, choosing and ordering components from a vocabulary you define, and a deterministic renderer maps that structure to real views. Change the user's intent and the screen recomposes, sometimes radically, but always inside a grammar you control.
This is the companion code for the post Generative UI on iOS: the model composes, you set the grammar.
Type an intent ("user profile", "login form", "league standings") and the app builds a screen for it. The same renderer draws every result. Only the data driving it changes.
On a real device with Apple Intelligence the structure is composed by the Foundation Models on-device model. Everywhere else, the app falls back to a deterministic mock so the demo still runs.
- Xcode 26 or 27
- Target iOS 26 or later
- For the real model: a physical device with Apple Intelligence enabled. The on-device model is not reliable in the Simulator, so the project forces the mock there at compile time.
- Create a new SwiftUI app.
- Drop
GenerativeUIDemo.swiftinto the project. - Set
GenerativeUIDemo()as the root view. - Build and run.
In the Simulator you get the mock immediately. The suggestion chips show three or four different layouts produced by the same renderer. To exercise the actual model, run on a supported device and type a free-form intent that does not match a canned keyword.
The whole contract between the model and the app is one enum. Each case is something the renderer knows how to draw. Adding a case widens what the model can invent. Removing one narrows the blast radius.
@Generable
enum UIComponent: Equatable {
case heading(text: String)
case paragraph(text: String)
case keyValue(key: String, value: String)
case badge(label: String, tone: Tone)
case field(label: String, placeholder: String)
case button(label: String, style: ActionStyle)
case standings(title: String, rows: [StandingRow])
case divider
}Components are grouped into sections, sections into a UIScreen. The @Generable macro and guided generation guarantee the model returns a valid instance of that type, so there is no JSON parsing and no defensive string handling. You ask for a UIScreen, you get a UIScreen.
Generation sits behind a protocol, which keeps the model swappable and the failure path clean:
protocol ScreenGenerating {
func generate(from intent: String) async throws -> UIScreen
}Three implementations:
FoundationModelsScreenGeneratorcalls the real model.MockScreenGeneratorreturns canned screens, deterministically.ResilientScreenGeneratorwraps a primary generator and falls back to the mock if it throws, so a model failure degrades gracefully instead of surfacing an error domain to the user.
makeScreenGenerator() picks the right one for the environment: mock in the Simulator, the resilient pair on a capable device, mock again when Apple Intelligence is unavailable.
From iOS 27 the model is a parameter. Any provider that adopts the LanguageModel protocol plugs into the same LanguageModelSession, so the @Generable types, the renderer, and the generation protocol do not change.
struct FoundationModelsScreenGenerator: ScreenGenerating {
var model: any LanguageModel = SystemLanguageModel.default
// ...
}Switching to Claude or Gemini is a Swift Package dependency and one argument, for example a model from Anthropic's package or FirebaseAI.firebaseAI().geminiLanguageModel(name:). Note that guided generation is part of the protocol surface but a provider may not support it and can throw unsupportedCapability(.guidedGeneration), which is exactly what the resilient fallback is there to catch.
The on-device model defaults to greedy sampling, so the same prompt yields the same structure. That is the sensible default for most tasks and makes output testable. For a generative UI you usually want variety, which you opt into through GenerationOptions by raising the temperature, switching to random sampling, and varying the seed on each call. Random sampling with a fixed seed stays deterministic for the same prompt.
let options = GenerationOptions(
sampling: .random(top: 20, seed: UInt64.random(in: .min ... .max)),
temperature: 0.9
)- The grammar is a ceiling. Ask for something the vocabulary cannot express, a chart or a map, and the model approximates with what it has. It cannot compose a component you did not define. That is the property that makes the output reasonable about.
- The model is not a knowledge base. It is a small on-device language model, not a source of facts. The standings in the demo are invented. In production, data has to come from a real source (a
Toolcall or your own layer), and the model, at most, lays it out. - Variety fights testability. Raising the temperature makes the same intent irreproducible, which complicates snapshot tests. Keep variety opt-in, and pin model, seed, and schema version when you need golden tests.
Foundation Models is a young framework and its API surface is still moving between iOS 26 and 27. If a signature does not match your SDK (for example the argument order on respond(to:generating:options:)), trust the compiler over this code and adjust. The architecture is the point, not any single line.
MIT. Use it, change it, ship it.