AI coding agents like Claude Code, Cursor, and Codex write SwiftUI that compiles and looks fine on its own, then drifts across a project: one screen uses @Observable, the next uses ObservableObject, strings are hardcoded in one file and generated in another, and the agent reinvents a networking layer you already have. The cause is not a bad model or a bad prompt. It is that the agent has no durable memory of your project's conventions, so each generation makes a locally reasonable choice from the many valid SwiftUI patterns it learned. The fix is to give the agent those conventions in a form it reads every time.
This post explains why agents produce inconsistent SwiftUI, the four failure patterns you will actually see, and the concrete setup (an AGENTS.md, tool rule files, typed constants, and skill packs) that keeps every generation on the same rails as a project grows.
On this page
- Why agents drift
- The four failure patterns
- The fix: give the agent conventions
- A before and after
- Common questions
Why agents drift
An AI agent generates each response from your prompt plus whatever context it can see right now, not from a stable model of your codebase. Two things follow from that. First, SwiftUI has changed a lot: the training data mixes years of patterns, so NavigationView and NavigationStack, ObservableObject and @Observable, all look like valid answers. Second, without your rules in context, the agent cannot know that your project already standardized on one of them. So it picks a reasonable option, and picks a different reasonable option next time.
This is why "just prompt it better" does not scale. A one-off instruction fixes one file. The next session, or a different file the agent could not see, starts fresh. Consistency has to live in the repository where every generation reads it, not in your memory of what you told the agent last week. The related mechanics of building a paid app this way are in vibe coding a paid iOS app.
The four failure patterns
Across real projects, unguided agents drift in the same four ways:
1. Inconsistent state management. The agent alternates between the older ObservableObject pattern and the current Observation framework:
// What an unguided agent often reaches for:
class ProfileViewModel: ObservableObject {
@Published var name = ""
}
// The current pattern you want everywhere (iOS 17+):
@Observable
final class ProfileModel {
var name = ""
}Both compile. Mixed across a codebase, they make data flow inconsistent and every new contributor (human or agent) guess which to follow.
2. Deprecated or outdated APIs. Because old SwiftUI is all over the training data, agents reach for it:
NavigationView { ... } // deprecated
NavigationStack { ... } // currentThe app still runs, so these slip through code review and pile up as quiet technical debt.
3. Stringly-typed code. Agents love string literals, and every one is a runtime failure waiting to happen:
// Fragile: a typo is a crash or a blank label, found by a user
Image("app-icon")
Text("welcome_title")
// Typed via SwiftGen: a typo is a build error, found by the compiler
Asset.Images.appIcon.swiftUIImage
Text(L10n.Welcome.title)4. Reinventing established patterns. The agent writes a fresh URLSession call or re-derives entitlement logic instead of using the abstractions you already have:
// Reinvented each time:
let (data, _) = try await URLSession.shared.data(from: url)
// vs. the project's one networking surface:
let user = try await APIClient.shared.send(.currentUser)
// Ad hoc subscription checks vs. one entitlement API:
if SubscriptionManager.shared.hasEntitlement("pro") { ... }The fix: give the agent conventions
The through-line of all four patterns is missing project memory. You close the gap by putting the conventions in the repo, in the places agents read:
-
An
AGENTS.mdat the root. A tool-agnostic file that states the rules plainly. Most current agents read it; the ones that use their own format get a thin wrapper (CLAUDE.mdfor Claude Code,.cursor/rules/*.mdcfor Cursor) that points back to it. Setting this up is covered in AGENTS.md for iOS and Cursor rules for SwiftUI.# SwiftUI conventions - State: @Observable models + @State. Never ObservableObject/@Published. - Navigation: NavigationStack. Never NavigationView. - Strings and assets: generated L10n / Asset. Never string literals. - Networking: APIClient.shared. Never raw URLSession. - Entitlements: SubscriptionManager.shared.hasEntitlement(_:). -
Typed constants (SwiftGen). This is the strongest guardrail because it does not rely on the agent behaving. When assets and strings are generated Swift, a wrong string literal simply does not compile, so the stringly-typed failure mode becomes impossible rather than discouraged.
-
Skill or review packs. Agent skills that review generated SwiftUI for deprecated APIs, data-flow correctness, and accessibility catch drift the conventions file did not spell out. Pair them with Claude Code for iOS development.
-
File-header pointers. A comment header on every new file that names the conventions file keeps the agent oriented even deep into a large project, where the root
AGENTS.mdmay be far outside the current context window.
A before and after
The difference is not that a guided agent is smarter. It is that the space of "reasonable" choices has been narrowed to one. Before conventions, ten screens accumulate three state patterns, two navigation APIs, and a scattering of string literals, and every review is a cleanup. After, the agent reads the rules, the compiler enforces the typed constants, and the tenth screen looks like the first. The payoff compounds: the more code the agent writes, the more a consistent baseline is worth, because you are not paying down drift on every pass.
None of this requires a heavier prompt or a bigger model. It requires the rules to exist where the agent looks, and a couple of them (the typed constants especially) to be enforced by the build rather than by good intentions.
Common questions
Why does the same agent write different SwiftUI in different files?
Because it generates from the current context, not a stable model of your project. Without your conventions in that context, it picks a locally reasonable pattern each time, and SwiftUI has enough valid patterns that "reasonable" varies file to file.
Can I fix this with a better prompt?
Only for the current file. Prompts do not persist across sessions or reach files the agent cannot see. Durable consistency has to live in the repository (an AGENTS.md, rule files, typed constants) so every generation reads the same rules.
What is AGENTS.md?
A plain-language, tool-agnostic file at the repo root that states your project's conventions for AI agents. Most agents read it directly; others get a small wrapper file that points back to it, so one source of truth serves every tool.
Do typed constants really matter for AI code?
They are the single most effective guardrail, because they move a whole failure mode from runtime to compile time. When assets and strings are generated Swift, an agent that invents a string literal produces a build error instead of a crash a user finds later.
Does this slow the agent down?
No. The agent still writes at the same speed; it just writes consistent code, so you spend far less time in review reconciling patterns. The net effect is faster, because drift cleanup is where the real time goes.
Every one of these guardrails has to be set up before the agent writes its first line, which is exactly what Spaceport bakes into the projects it generates. Each SwiftUI project ships with an AGENTS.md, the matching CLAUDE.md and .cursor/rules wrappers, SwiftGen-generated typed constants for assets and strings, an auto-inserted file-header pointer on every new file, and a bundle of Swift review skills, so a coding agent inherits the conventions from the first file it touches rather than drifting until you notice. The generator also wires the paywall, subscriptions, and networking layer the agent is meant to reuse instead of reinvent. And when you are lining up a waitlist and launch-day audience for the app, our sister tool Lighthouse covers that side.
From an indie iOS dev, for indie iOS devs.