{"slug": "stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation", "title": "Stop Rebuilding the Same SwiftUI Components: A Guide to DesignFoundation", "summary": "NerdSnipe Inc. released DesignFoundation, an open-source SwiftUI package that provides a token-based theming engine, 25 components, and 6 feedback/overlay modifiers to prevent UI drift when building with AI coding agents. The package includes CLAUDE.md, AGENTS.md, and Cursor rules so agents consistently use DFButton instead of creating new button styles. DesignFoundation ships with four presets—slate, aurora, copper, and sage—and requires Xcode 16+, iOS 18+, or macOS 15+.", "body_md": "\"I'll just write a quick button style... and a text field with validation... and a card component...\" Two weeks later you have a bespoke design system that only half the app uses, three slightly different buttons, and nothing shipped.\n\nIf you're building with an AI coding agent, the drift problem is worse, not better. Ask Claude, Cursor, or Codex to \"add a settings screen\" three separate times and you'll get three different paddings, three different corner radii, and a button style that quietly forked itself somewhere around commit 40. Agents are great at writing plausible SwiftUI and bad at remembering what the rest of your app already decided.\n\n[DesignFoundation](https://github.com/NerdSnipe-Inc/design-foundation) is an open-source SwiftUI package that gives you a token-based theming engine, 25 components, and 6 feedback/overlay modifiers that all read from the same theme. You set it once at the app root, and everything underneath updates. That's the whole idea — and it ships with `CLAUDE.md`\n\n, `AGENTS.md`\n\n, and Cursor rules already written, so an agent working in your repo reaches for `DFButton`\n\ninstead of inventing a new one.\n\nIn this tutorial you'll go from zero to a themed, consistent SwiftUI UI — without writing a single custom button style.\n\nBy the end you'll have:\n\n**Requirements:** Xcode 16+, iOS 18+ / macOS 15+ target, Swift 6.\n\n`https://github.com/NerdSnipe-Inc/design-foundation`\n\n`1.0.0`\n\n`DesignFoundation`\n\nto your target\n\n```\ndependencies: [\n    .package(url: \"https://github.com/NerdSnipe-Inc/design-foundation\", from: \"1.0.0\")\n],\ntargets: [\n    .target(name: \"YourApp\", dependencies: [\"DesignFoundation\"])\n]\n```\n\nEvery app that grows past a prototype ends up with some version of a `ThemeManager`\n\nsingleton — a global object holding colors and fonts that every view reaches into, and that somebody eventually has to thread through previews, tests, and SwiftUI's environment by hand.\n\nImport `DesignFoundation`\n\nand attach a preset theme to your root scene instead. That's all the wiring needed — every component below this point reads from it.\n\n``` python\nimport SwiftUI\nimport DesignFoundation\n\n@main\nstruct MyApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n                .dfThemePreset(.slate)\n        }\n    }\n}\n```\n\nFour general-purpose presets ship out of the box:\n\n| Preset | Character | Primary (light / dark) | Works well for |\n|---|---|---|---|\n`.slate` |\nProfessional, balanced, default radius |\n`#1C3D5A` / `#64B5F6`\n|\nSaaS dashboards, dev tools |\n`.aurora` |\nVibrant, rounded, electric violet |\n`#6C47FF` / `#A78BFA`\n|\nCreative tools, social apps |\n`.copper` |\nWarm, editorial, sharp radius |\n`#C4622D` / `#F4A261`\n|\nFinance, content readers |\n`.sage` |\nCalm, organic, very rounded |\n`#2D6A4F` / `#74C69D`\n|\nHealth, wellness |\n\nEach preset automatically switches between its light and dark variant based on `@Environment(\\.colorScheme)`\n\n. You don't manage that yourself.\n\nThe second conversation every SwiftUI project has with itself: \"should this button be `.borderedProminent`\n\nor a custom `ButtonStyle`\n\n, and does the disabled state look right, and did I remember to match the corner radius everywhere else?\" You answer those questions once, then answer them again for the next button.\n\nOnce the theme is in the environment, you can drop in any `DF*`\n\ncomponent instead. They all read tokens from the theme — no manual color or font passing required.\n\n``` js\nstruct ContentView: View {\n    var body: some View {\n        VStack(spacing: 24) {\n            DFText(\"Welcome back\", scale: .title)\n            DFText(\"Sign in to continue\", scale: .body)\n            DFButton(\"Sign in\") {\n                // action\n            }\n        }\n        .padding()\n    }\n}\n```\n\n`DFText`\n\nuses the theme's typography tokens. `DFButton`\n\nuses color, radius, and spacing tokens. Change the preset at the app root and both update.\n\n`DFButton`\n\nships with five built-in styles:\n\n```\nDFButton(\"Primary action\") { }          // .filled (default)\nDFButton(\"Secondary\") { }\n    .dfButtonStyle(.outlined)\nDFButton(\"Subtle\") { }\n    .dfButtonStyle(.ghost)\nDFButton(\"Tinted\") { }\n    .dfButtonStyle(.tinted)\nDFButton(\"Glass\") { }\n    .dfButtonStyle(.glass)              // requires iOS 26+ / macOS 26+\n```\n\nApply a style to an entire section instead of to each button individually:\n\n```\nVStack {\n    DFButton(\"Save\") { }\n    DFButton(\"Cancel\") { }\n}\n.dfButtonStyle(.outlined)\n```\n\nValidation UI is deceptively fiddly: where does the error text sit, does it push the layout down or overlay it, does the border turn red before or after the user leaves the field. Most teams solve this differently in every form in the app.\n\nDesignFoundation's input components share a `DFValidationState`\n\ntype so error states look consistent everywhere.\n\n``` js\nstruct SignInView: View {\n    @State private var email = \"\"\n    @State private var password = \"\"\n    @State private var emailState: DFValidationState = .none\n    @State private var passwordState: DFValidationState = .none\n\n    var body: some View {\n        VStack(spacing: 16) {\n            DFTextField(\"Email\", text: $email, validationState: emailState)\n                .dfTextFieldStyle(.outlined)\n\n            DFSecureField(\"Password\", text: $password, validationState: passwordState)\n                .dfTextFieldStyle(.outlined)\n\n            DFButton(\"Sign in\") {\n                validate()\n            }\n        }\n        .padding()\n    }\n\n    private func validate() {\n        emailState = email.contains(\"@\")\n            ? .valid\n            : .error(\"Enter a valid email address\")\n\n        passwordState = password.count >= 8\n            ? .valid\n            : .error(\"Password must be at least 8 characters\")\n    }\n}\n```\n\n`DFValidationState`\n\nhas three cases: `.none`\n\n, `.valid`\n\n, and `.error(String)`\n\n. The error message renders inline under the field automatically — you don't lay it out yourself.\n\n`DFSecureField`\n\nalso includes a built-in reveal toggle. No extra code needed.\n\n\"What shadow radius did we use for cards again?\" is a question that shouldn't need a Slack search. `DFCard`\n\nreads elevation from the same theme as everything else, so every card in the app — dashboard tile, list row, modal — gets the same shadow, radius, and surface color without you copy-pasting a `.shadow()`\n\nmodifier around the codebase.\n\n```\nDFCard {\n    VStack(alignment: .leading, spacing: 12) {\n        DFText(\"Account summary\", scale: .headline)\n        DFDivider()\n        HStack {\n            DFText(\"Status\", scale: .body)\n            Spacer()\n            DFBadge(text: \"Active\")\n                .dfBadgeStyle(.tinted)\n        }\n    }\n    .padding()\n}\n.dfCardStyle(.elevated)\n```\n\nCard styles: `.elevated`\n\n, `.outlined`\n\n, `.filled`\n\n, `.glass`\n\n(iOS 26+).\n\nToast queues in particular tend to grow their own bespoke state machine — an array of pending messages, timers to dismiss them, logic to avoid three toasts stacking on top of each other. DesignFoundation ships that machinery so you don't own it.\n\nMount `.dfToast()`\n\nonce on any root view, then trigger toasts imperatively from anywhere in the tree:\n\n```\n// Mount once at the root\nContentView()\n    .dfToast()\n\n// Trigger from any view action\nDFButton(\"Save changes\") {\n    // do the save\n    DFToastQueue.shared.show(text: \"Changes saved\", severity: .success)\n}\n```\n\n`DFToastQueue`\n\nhandles queue management and auto-dismiss. Multiple toasts queued in quick succession display in order.\n\n```\nif isLoading {\n    DFSkeleton()\n        .frame(height: 80)\n} else {\n    // actual content\n}\n```\n\n`DFSkeleton`\n\nplays a shimmer animation. Use it as a placeholder while data loads.\n\n```\nDFProgressBar(value: uploadProgress)              // linear, determinate\nDFProgressBar(variant: .indeterminate)            // indeterminate\n```\n\nThe four presets cover a lot of ground, but sometimes you need your brand colors. `DFTheme`\n\nexposes token namespaces for colors, typography, spacing, radius, shadow, and animation.\n\n``` js\n@main\nstruct MyApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n                .dfTheme(DFTheme(\n                    colors: DFColorTokens(\n                        primary: Color(red: 0.388, green: 0.400, blue: 0.945)   // brand indigo, #6366F1\n                    ),\n                    spacing: DFSpacingTokens(md: 20),\n                    radius: DFRadiusTokens(md: 14)\n                ))\n        }\n    }\n}\n```\n\nDesignFoundation doesn't ship a\n\n`Color(hex:)`\n\ninitializer — use`Color(red:green:blue:)`\n\n, an asset catalog color, or your own hex-to-`Color`\n\nhelper if you're pasting values straight from a design tool.\n\nYou don't have to specify every token. Unspecified tokens fall back to the defaults.\n\nIf a preset is 95% right but one token is off, mutate just that token:\n\n``` js\nvar theme = DFTheme.slateLight\ntheme.colors.primary = .purple\n\nMyView()\n    .dfTheme(theme)\n```\n\nThe preset modifier handles switching automatically. If you need to force a specific variant — useful in Previews or sub-tree overrides — use the named theme directly:\n\n```\n// Force dark regardless of system setting\nMyView()\n    .dfTheme(.slateDark)\n```\n\nBecause `DFTheme`\n\nlives in SwiftUI's environment, you can scope overrides to any part of the view tree:\n\n```\nVStack {\n    // rest of the UI uses the app-level theme\n\n    SettingsPanel()\n        .dfTheme(.copperLight)   // this subtree uses copper\n}\n```\n\nThe alternative to a custom preset is remembering to apply your brand color twice — once for light mode, once for dark — everywhere you set a theme. `DFThemePreset`\n\ncollapses that into one named value:\n\n``` js\nlet brandPreset = DFThemePreset(\n    light: {\n        var t = DFTheme.slateLight\n        t.colors.primary = Color(red: 0.388, green: 0.400, blue: 0.945)   // #6366F1\n        return t\n    }(),\n    dark: {\n        var t = DFTheme.slateDark\n        t.colors.primary = Color(red: 0.506, green: 0.549, blue: 0.973)   // #818CF8\n        return t\n    }()\n)\n\nMyApp()\n    .dfThemePreset(brandPreset)\n```\n\nWithout a shared style system, \"make every button on this screen ghost-style\" means visiting every button. With one, it's one modifier on the container.\n\nThe style pattern follows the same protocol SwiftUI uses for `ButtonStyle`\n\n. Styles compose, propagate through the environment, and apply hierarchically.\n\n```\n// Apply styles to a whole section\nVStack { ... }\n    .dfButtonStyle(.outlined)\n    .dfCardStyle(.glass)\n\n// Override for one component\nDFButton(\"Delete\", role: .destructive) { }\n    .dfButtonStyle(.ghost)\n```\n\nIf you want Liquid Glass across your entire UI (iOS 26+ / macOS 26+):\n\n```\nContentView()\n    .dfButtonStyle(.glass)\n    .dfCardStyle(.glass)\n    .dfTooltipStyle(.glass)\n```\n\nDesignFoundation targets iOS 18+, macOS 15+, and visionOS 2+. Most components work across all three. A few notes:\n\n`.glass`\n\nstyles require iOS 26+ / macOS 26+ (Liquid Glass). You get a compile-time reminder if you use them on an older target.`DFSidebar`\n\nis only meaningful on macOS and iPadOS.`DFPlatformVariant`\n\n(`.automatic / .compact / .expanded / .immersive`\n\n) lets components adapt at runtime, or lets you force a layout for Previews and testing.The part worth calling out: **you do not need #if os(macOS) / #if os(iOS) to use any DF component.** Platform differences — sidebar vs. tab bar, desktop vs. touch density, native table vs. scrollable rows — are handled internally by\n\n`DFPlatformContext`\n\n, injected automatically by `.dfTheme()`\n\n. Your own view code stays platform-agnostic; the only place you still reach for a guard is app-level APIs the package doesn't wrap, like a Mac-only `WindowGroup`\n\n.If you're using Claude Code, Cursor, or another agent to build UI, DesignFoundation ships instructions for all three: `CLAUDE.md`\n\n, `AGENTS.md`\n\n, and `.cursor/rules/design-foundation.mdc`\n\nsit at the package root. Point an agent at a repo that depends on DesignFoundation and it already knows the rule — reach for `DFButton`\n\n, `DFCard`\n\n, `DFTextField`\n\n, not a hand-rolled equivalent — without you writing that instruction yourself.\n\nThat matters more for agents than for you. A human developer remembers the button style they used last week; an agent starting a fresh session doesn't, and will happily invent a fourth `ButtonStyle`\n\nif nothing tells it otherwise. Combined with the platform-agnostic API above, this means an agent can build a real cross-platform screen — no `#if os()`\n\nblocks, no reinvented button — in one pass, and the result is something you can actually debug later, because every screen it touched is made of the same handful of known components instead of N slightly different one-offs.\n\nA quick reference of everything that ships:\n\n**Primitives:** `DFButton`\n\n, `DFText`\n\n, `DFIcon`\n\n, `DFBadge`\n\n, `DFAvatar`\n\n, `DFDivider`\n\n**Inputs:** `DFTextField`\n\n, `DFSecureField`\n\n, `DFTextArea`\n\n, `DFToggle`\n\n, `DFSlider`\n\n, `DFPicker`\n\n, `DFDatePicker`\n\n, `DFCheckbox`\n\n**Layout:** `DFCard`\n\n**Overlays** (modifiers, not constructible views): `.dfModal()`\n\n, `.dfSheet()`\n\n, `.dfPopover()`\n\n, `.dfTooltip()`\n\n**Navigation:** `DFTabBar`\n\n, `DFNavigationBar`\n\n, `DFSidebar`\n\n**Supplementary:** `DFAlertConfiguration`\n\n+ `.dfAlert()`\n\n, `DFToastQueue`\n\n+ `.dfToast()`\n\n, `DFSkeleton`\n\n, `DFProgressBar`\n\n, `DFList`\n\n, `DFListRow`\n\n, `DFTable`\n\n, `DFDataTable`\n\n, `DFDataGrid`\n\nDesignFoundation solves the problem most SwiftUI projects hit at month three: component drift, where three different developers have built three slightly different buttons and nobody's sure which one is \"correct\" anymore. When every component reads from the same `DFTheme`\n\nin the environment, a brand refresh is a token edit, not a project-wide find-and-replace.\n\nThe package is MIT licensed and actively maintained. The [GitHub repo](https://github.com/NerdSnipe-Inc/design-foundation) is the right place for issues or feedback, especially if the theme API feels rough for your use case.\n\nIf you need pre-built screens (auth flows, dashboards, CRM layouts) composed from these same primitives, there's a [pro tier](https://nerdsnipe-inc.github.io/design-foundation/pro/) — but the primitives in this tutorial stay free.\n\n*Have a question about a specific component or want to see a more advanced theming example? Drop it in the comments.*", "url": "https://wpnews.pro/news/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation", "canonical_source": "https://dev.to/nerd_snipe_dev/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation-11a1", "published_at": "2026-07-07 12:39:42+00:00", "updated_at": "2026-07-07 12:58:25.873190+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "large-language-models"], "entities": ["NerdSnipe Inc.", "DesignFoundation", "Claude", "Cursor", "Codex", "SwiftUI", "Xcode", "iOS"], "alternates": {"html": "https://wpnews.pro/news/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation", "markdown": "https://wpnews.pro/news/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation.md", "text": "https://wpnews.pro/news/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation.txt", "jsonld": "https://wpnews.pro/news/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation.jsonld"}}