If you've pointed an AI coding agent at a SwiftUI project recently, you've probably watched it do something slightly insane: reinvent a button. Then a card. Then a form field with its own ad-hoc validation state. Every fresh session, every new view, the same primitives get rebuilt from scratch — not because the model is bad at SwiftUI, but because it has no idea a button already exists in your project that it's supposed to reuse. That costs you tokens, and it costs your app visual consistency, because "rebuilt from scratch" rarely matches "built five files ago."
That problem got sharper for me this year with iPhone Duo entering the picture. Now I'm not just keeping a button consistent across light/dark mode — I'm keeping a whole screen's layout sane across a flat phone, a folded phone with a screen divided by a hinge, and a Mac. Writing #if os(iOS) branches and hand-rolled GeometryReader math for every screen that needs to adapt isn't just slow, it's exactly the kind of repetitive, error-prone work that should not be re-derived by an LLM on every prompt.
So here's the pattern I landed on: a token-based SwiftUI design system (DesignFoundation, MIT-licensed) with a compile-checked API reference file an agent can actually read, plus a couple of primitives purpose-built for the fold. This post walks through the fold-handling part, and how having a canonical component reference changes what an AI coding session looks like.
The actual layout problem
iPhone Duo (Xcode 27.1+) splits the display around a physical hinge. A view that spans the whole screen needs to know whether it's being shown flat, folded, or on a completely different device class — and it needs to do that without UIScreen.main or a pile of size-class guesswork, because the fold isn't a size class.
Here's a two-pane layout that "just divides" around the fold when one exists, and falls back to a normal adaptive split everywhere else:
import DesignFoundation
struct InboxView: View {
var body: some View {
DFArrangement {
MessageListView()
} secondary: {
MessageDetailView()
}
}
}
That's the entire call. DFArrangement resolves to SwiftUI's ArrangementView on Xcode 27.1+ (so it divides around the iPhone Duo fold automatically), and on every earlier SDK and OS version it's a plain stack that follows the same .split rule — side by side when there's room, stacked when there isn't. The iPhone Duo-specific symbols are compiled out with #if compiler(>=6.4) && canImport(SwiftUI, _version: 8.1) inside the package, so your own call site never needs a single availability guard.
When you need the actual geometry — say, to avoid placing content under the hinge — you read it instead of guessing it:
DFReservedRegionReader { regions in
VStack {
Text(regions.isSplit ? "Folded" : "Flat")
if let axis = regions.splitAxis {
Text(axis == .horizontal ? "Panes sit side by side" : "Panes are stacked")
}
}
}
DFReservedRegions is a plain value type under the hood — regions, divisions, occlusions, isSplit, splitAxis, and a panes(in:) helper that hands back the two rects on either side of the fold. Because it's pure data, you can unit test fold behavior with a made-up CGRect and never touch a simulator:
let fold = DFReservedRegion(kind: .division, frame: CGRect(x: 396, y: 0, width: 8, height: 600))
let panes = DFReservedRegions([fold]).panes(in: CGRect(x: 0, y: 0, width: 800, height: 600))
One more detail that's easy to miss until you've shipped it and gotten a bug report: on iPhone Duo, the outer display (and some inner-display positions) renders nav bars, toolbars and tab bars running vertically down a side instead of across the top or bottom. DFPlatformContext.toolbarVerticalEdge tells you which side, or nil on everything before the 27.1 SDK:
@Environment(\.dfPlatformContext) private var platform
var body: some View {
Text(platform.hasVerticalToolbar ? "Bars run down a side" : "Bars run across the top and bottom")
}
None of this requires you to special-case iPhone Duo in application code. The package absorbs the branching; your views stay the same on every platform.
Why a CLAUDE.md changes what happens in an agent session
Here's the part that actually matters for the "AI coders waste tokens" problem. DesignFoundation ships a CLAUDE.md — a compile-checked file (every code snippet in it is verified in CI) that states the rule up front:
Never build UI components that DesignFoundation already provides.
and then gives the exact, correct signature for all 54 components, every style variant, every token namespace. The difference this makes in practice: instead of an agent inferring a plausible-looking DFButton API from vibes and getting the parameter order wrong, it reads the real one — DFButton("Save", style: .outlined) { action() }, role: .destructive as a separate param, not a style — and writes it correctly the first time. One DFCard { DFText("Welcome", scale: .title) } replaces the 20-plus lines of custom RoundedRectangle + shadow + font styling an agent would otherwise write per-screen, per-session, slightly differently every time.
That consistency compounds specifically on multi-platform builds. A theme set once at the app root —
HomeView()
.dfToast()
.dfThemePreset(.slate)
— reaches every component underneath it, on iPhone, iPhone Duo, and macOS, because platform differences are resolved internally via DFPlatformContext rather than scattered #if os() branches across your (or your agent's) code. Change the theme file, the whole app restyles. No file hunt six months later to find where someone hardcoded a corner radius.
A small Duo-ready screen, end to end
Putting it together — a themed inbox that adapts to the fold with validated state and a real toast on send:
import SwiftUI
import DesignFoundation
struct ComposeView: View {
@Environment(\.dfTheme) private var theme
@State private var subject = ""
@State private var body = ""
var formState = DFFormState(fields: [
"subject": [DFRequiredValidator()],
])
var content: some View {
DFCard {
VStack(alignment: .leading, spacing: theme.spacing.md) {
DFText("New message", scale: .title)
DFValidatedTextField("Subject", field: "subject", form: formState)
DFTextArea("Body", text: $body, placeholder: "Say something…")
DFButton("Send") {
guard formState.validate() else { return }
DFToastQueue.shared.show(text: "Sent", severity: .success)
}
}
}
.padding(theme.spacing.lg)
}
var body: some View {
DFArrangement {
MessageListView()
} secondary: {
content
}
}
}
That's a validated form, a themed card, and a fold-aware layout, with zero platform branching and zero custom styling code. If you're building a list-beside-detail screen like this often enough that you'd rather not wire up DFArrangement yourself every time, DesignFoundation Pro ships six finished Duo layouts on the same primitive — DFNewsDuoLayout, DFCRMDuoLayout, DFEcommerceOrdersDuoLayout, DFPMDuoLayout, DFDocumentsDuoLayout and DFAIChatDuoLayout — each one just a composed DFDuoMasterDetail container wired to a real vertical's data model, so DFCRMDuoLayout() with defaults gets you contacts-beside-detail with zero wiring.
Try it
- DesignFoundation — free, MIT-licensed, 54 components, iOS 18+/macOS 15+/visionOS 2+
- Docs site and DFPlayground — browse every component and theme preset live, free, macOS
- DesignFoundation Pro — the paid tier: 30 blocks, 55 screens across 12 verticals, 18 navigation shells, and the 6 Duo layouts above
- Sponsor the free package if it saved you the two weeks it used to cost me, every project, before I stopped rebuilding the same card for the fifth time.
Top comments (0)