Ask an AI coding assistant to build five screens for the same SwiftUI app, one session at a time, and you'll usually get five screens that almost match. Same rough layout, different corner radii, different spacing scale, a button style that drifted because the assistant re-derived "what this app's style should be" from scratch each time instead of reading it from somewhere.
A better prompt won't fix that. What fixes it is giving the assistant one place to read styling from, so it never has to guess.
The problem: style lives in the prompt, not in the code
When you ask an AI assistant to build a screen, it has two choices for where visual style comes from:
- Infer it from whatever other files it can see in context.
- Invent something reasonable-looking from training data.
Neither is a single source of truth. If screen A was built in a session where the assistant saw your onboarding flow, and screen B was built in a session where it didn't, the two screens will diverge — different padding, different type scale, sometimes a different corner radius token entirely. You end up re-explaining your design language every session, which burns tokens on styling instructions that have nothing to do with the actual feature you're building.
The fix: a theme type the assistant can read instead of guess
The reusable technique is simple: define one Theme type (or DesignTokens, or whatever you want to call it) that holds every value a screen is allowed to style itself with — colors, spacing scale, corner radii, type styles — and make every view read from an injected instance of it instead of hardcoding values.
struct Theme: Sendable {
var spacing = Spacing()
var corner = CornerRadii()
var colors = Palette()
var type = Typography()
static let `default` = Theme()
}
struct Spacing: Sendable {
var xs: CGFloat = 4
var sm: CGFloat = 8
var md: CGFloat = 16
var lg: CGFloat = 24
}
// CornerRadii, Palette and Typography follow the same shape:
// a small Sendable struct of named values with defaults.
Inject it through the environment so any view can reach it without threading it through every initializer:
private struct ThemeKey: EnvironmentKey {
static let defaultValue = Theme.default
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
(On iOS 18 / Xcode 16 and later, the @Entry macro collapses this to one line: extension EnvironmentValues { @Entry var theme: Theme = .default }.)
Then every screen — whether a human wrote it or an AI assistant did — pulls values from @Environment(\.theme) var theme instead of writing .padding(16) or Color(hex: "0A84FF") directly. The number 16 and the hex code stop existing as magic values scattered across files; they exist exactly once, in the theme.
Why this specifically helps an AI assistant
You get less repetition, but the bigger win is that a theme type is something an assistant can read instead of infer. When you ask it to build a new screen, you can point it at the Theme struct and the handful of views that already use it, and it has a concrete contract to follow — these are the only spacing values, these are the only colors, this is what the button style actually is. It doesn't need the prior session's context, and it doesn't need to pattern-match against four other screens to figure out what "normal" looks like for this app. There's exactly one place normal is defined.
This also makes drift a code-review problem instead of a visual-review problem. If a new screen uses .padding(20) instead of theme.spacing.lg, that's a plain diff to catch — not something you notice three weeks later when two screens look subtly off next to each other.
Taking it further: compile-checked agent instructions
Once you have a theme type, you can go one step further and write your AI-assistant instructions (an AGENTS.md or CLAUDE.md) as compiled code snippets rather than prose describing the rules. Instead of telling the assistant "always use theme.spacing values, never hardcode padding," include an actual Swift snippet in your instructions file that demonstrates the pattern, and run that snippet through your test suite or CI so it can't silently go stale. Prose instructions drift from the real codebase over time without anyone noticing; a snippet that fails to compile is a build break, which someone will actually see.
This is the same idea DesignFoundationPro applies at a larger scale — one DFTheme behind 55 prebuilt screens and 30 UI blocks, with its own agent-reference file compiled in CI — but the underlying technique works in a from-scratch app with a single Theme struct and twenty minutes of setup. A theme an AI assistant can read is worth more than any amount of styling instructions it has to re-derive every session.
Top comments (0)