cd /news/developer-tools/stop-rebuilding-the-same-swiftui-com… · home topics developer-tools article
[ARTICLE · art-49394] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

Stop Rebuilding the Same SwiftUI Components: A Guide to DesignFoundation

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+.

read10 min views1 publishedJul 7, 2026

"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.

If 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.

DesignFoundation 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

, AGENTS.md

, and Cursor rules already written, so an agent working in your repo reaches for DFButton

instead of inventing a new one.

In this tutorial you'll go from zero to a themed, consistent SwiftUI UI — without writing a single custom button style.

By the end you'll have:

Requirements: Xcode 16+, iOS 18+ / macOS 15+ target, Swift 6.

https://github.com/NerdSnipe-Inc/design-foundation

1.0.0

DesignFoundation

to your target

dependencies: [
    .package(url: "https://github.com/NerdSnipe-Inc/design-foundation", from: "1.0.0")
],
targets: [
    .target(name: "YourApp", dependencies: ["DesignFoundation"])
]

Every app that grows past a prototype ends up with some version of a ThemeManager

singleton — 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.

Import DesignFoundation

and attach a preset theme to your root scene instead. That's all the wiring needed — every component below this point reads from it.

import SwiftUI
import DesignFoundation

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .dfThemePreset(.slate)
        }
    }
}

Four general-purpose presets ship out of the box:

Preset Character Primary (light / dark) Works well for
.slate
Professional, balanced, default radius
#1C3D5A / #64B5F6
SaaS dashboards, dev tools
.aurora
Vibrant, rounded, electric violet
#6C47FF / #A78BFA
Creative tools, social apps
.copper
Warm, editorial, sharp radius
#C4622D / #F4A261
Finance, content readers
.sage
Calm, organic, very rounded
#2D6A4F / #74C69D
Health, wellness

Each preset automatically switches between its light and dark variant based on @Environment(\.colorScheme)

. You don't manage that yourself.

The second conversation every SwiftUI project has with itself: "should this button be .borderedProminent

or a custom ButtonStyle

, 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.

Once the theme is in the environment, you can drop in any DF*

component instead. They all read tokens from the theme — no manual color or font passing required.

struct ContentView: View {
    var body: some View {
        VStack(spacing: 24) {
            DFText("Welcome back", scale: .title)
            DFText("Sign in to continue", scale: .body)
            DFButton("Sign in") {
                // action
            }
        }
        .padding()
    }
}

DFText

uses the theme's typography tokens. DFButton

uses color, radius, and spacing tokens. Change the preset at the app root and both update.

DFButton

ships with five built-in styles:

DFButton("Primary action") { }          // .filled (default)
DFButton("Secondary") { }
    .dfButtonStyle(.outlined)
DFButton("Subtle") { }
    .dfButtonStyle(.ghost)
DFButton("Tinted") { }
    .dfButtonStyle(.tinted)
DFButton("Glass") { }
    .dfButtonStyle(.glass)              // requires iOS 26+ / macOS 26+

Apply a style to an entire section instead of to each button individually:

VStack {
    DFButton("Save") { }
    DFButton("Cancel") { }
}
.dfButtonStyle(.outlined)

Validation 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.

DesignFoundation's input components share a DFValidationState

type so error states look consistent everywhere.

struct SignInView: View {
    @State private var email = ""
    @State private var password = ""
    @State private var emailState: DFValidationState = .none
    @State private var passwordState: DFValidationState = .none

    var body: some View {
        VStack(spacing: 16) {
            DFTextField("Email", text: $email, validationState: emailState)
                .dfTextFieldStyle(.outlined)

            DFSecureField("Password", text: $password, validationState: passwordState)
                .dfTextFieldStyle(.outlined)

            DFButton("Sign in") {
                validate()
            }
        }
        .padding()
    }

    private func validate() {
        emailState = email.contains("@")
            ? .valid
            : .error("Enter a valid email address")

        passwordState = password.count >= 8
            ? .valid
            : .error("Password must be at least 8 characters")
    }
}

DFValidationState

has three cases: .none

, .valid

, and .error(String)

. The error message renders inline under the field automatically — you don't lay it out yourself.

DFSecureField

also includes a built-in reveal toggle. No extra code needed.

"What shadow radius did we use for cards again?" is a question that shouldn't need a Slack search. DFCard

reads 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()

modifier around the codebase.

DFCard {
    VStack(alignment: .leading, spacing: 12) {
        DFText("Account summary", scale: .headline)
        DFDivider()
        HStack {
            DFText("Status", scale: .body)
            Spacer()
            DFBadge(text: "Active")
                .dfBadgeStyle(.tinted)
        }
    }
    .padding()
}
.dfCardStyle(.elevated)

Card styles: .elevated

, .outlined

, .filled

, .glass

(iOS 26+).

Toast 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.

Mount .dfToast()

once on any root view, then trigger toasts imperatively from anywhere in the tree:

// Mount once at the root
ContentView()
    .dfToast()

// Trigger from any view action
DFButton("Save changes") {
    // do the save
    DFToastQueue.shared.show(text: "Changes saved", severity: .success)
}

DFToastQueue

handles queue management and auto-dismiss. Multiple toasts queued in quick succession display in order.

if is {
    DFSkeleton()
        .frame(height: 80)
} else {
    // actual content
}

DFSkeleton

plays a shimmer animation. Use it as a placeholder while data loads.

DFProgressBar(value: uploadProgress)              // linear, determinate
DFProgressBar(variant: .indeterminate)            // indeterminate

The four presets cover a lot of ground, but sometimes you need your brand colors. DFTheme

exposes token namespaces for colors, typography, spacing, radius, shadow, and animation.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .dfTheme(DFTheme(
                    colors: DFColorTokens(
                        primary: Color(red: 0.388, green: 0.400, blue: 0.945)   // brand indigo, #6366F1
                    ),
                    spacing: DFSpacingTokens(md: 20),
                    radius: DFRadiusTokens(md: 14)
                ))
        }
    }
}

DesignFoundation doesn't ship a

Color(hex:)

initializer — useColor(red:green:blue:)

, an asset catalog color, or your own hex-to-Color

helper if you're pasting values straight from a design tool.

You don't have to specify every token. Unspecified tokens fall back to the defaults.

If a preset is 95% right but one token is off, mutate just that token:

var theme = DFTheme.slateLight
theme.colors.primary = .purple

MyView()
    .dfTheme(theme)

The 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:

// Force dark regardless of system setting
MyView()
    .dfTheme(.slateDark)

Because DFTheme

lives in SwiftUI's environment, you can scope overrides to any part of the view tree:

VStack {
    // rest of the UI uses the app-level theme

    SettingsPanel()
        .dfTheme(.copperLight)   // this subtree uses copper
}

The 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

collapses that into one named value:

let brandPreset = DFThemePreset(
    light: {
        var t = DFTheme.slateLight
        t.colors.primary = Color(red: 0.388, green: 0.400, blue: 0.945)   // #6366F1
        return t
    }(),
    dark: {
        var t = DFTheme.slateDark
        t.colors.primary = Color(red: 0.506, green: 0.549, blue: 0.973)   // #818CF8
        return t
    }()
)

MyApp()
    .dfThemePreset(brandPreset)

Without 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.

The style pattern follows the same protocol SwiftUI uses for ButtonStyle

. Styles compose, propagate through the environment, and apply hierarchically.

// Apply styles to a whole section
VStack { ... }
    .dfButtonStyle(.outlined)
    .dfCardStyle(.glass)

// Override for one component
DFButton("Delete", role: .destructive) { }
    .dfButtonStyle(.ghost)

If you want Liquid Glass across your entire UI (iOS 26+ / macOS 26+):

ContentView()
    .dfButtonStyle(.glass)
    .dfCardStyle(.glass)
    .dfTooltipStyle(.glass)

DesignFoundation targets iOS 18+, macOS 15+, and visionOS 2+. Most components work across all three. A few notes:

.glass

styles require iOS 26+ / macOS 26+ (Liquid Glass). You get a compile-time reminder if you use them on an older target.DFSidebar

is only meaningful on macOS and iPadOS.DFPlatformVariant

(.automatic / .compact / .expanded / .immersive

) 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

DFPlatformContext

, injected automatically by .dfTheme()

. 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

.If you're using Claude Code, Cursor, or another agent to build UI, DesignFoundation ships instructions for all three: CLAUDE.md

, AGENTS.md

, and .cursor/rules/design-foundation.mdc

sit at the package root. Point an agent at a repo that depends on DesignFoundation and it already knows the rule — reach for DFButton

, DFCard

, DFTextField

, not a hand-rolled equivalent — without you writing that instruction yourself.

That 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

if 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()

blocks, 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.

A quick reference of everything that ships:

Primitives: DFButton

, DFText

, DFIcon

, DFBadge

, DFAvatar

, DFDivider

Inputs: DFTextField

, DFSecureField

, DFTextArea

, DFToggle

, DFSlider

, DFPicker

, DFDatePicker

, DFCheckbox

Layout: DFCard

Overlays (modifiers, not constructible views): .dfModal()

, .dfSheet()

, .dfPopover()

, .dfTooltip()

Navigation: DFTabBar

, DFNavigationBar

, DFSidebar

Supplementary: DFAlertConfiguration

  • .dfAlert()

, DFToastQueue

  • .dfToast()

, DFSkeleton

, DFProgressBar

, DFList

, DFListRow

, DFTable

, DFDataTable

, DFDataGrid

DesignFoundation 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

in the environment, a brand refresh is a token edit, not a project-wide find-and-replace.

The package is MIT licensed and actively maintained. The GitHub repo is the right place for issues or feedback, especially if the theme API feels rough for your use case.

If you need pre-built screens (auth flows, dashboards, CRM layouts) composed from these same primitives, there's a pro tier — but the primitives in this tutorial stay free.

Have a question about a specific component or want to see a more advanced theming example? Drop it in the comments.

── more in #developer-tools 4 stories · sorted by recency
── more on @nerdsnipe inc. 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/stop-rebuilding-the-…] indexed:0 read:10min 2026-07-07 ·