DEV Community

Dheerendra Chaturvedi
Dheerendra Chaturvedi

Posted on AI-assisted

DesignSystemKit: one design system for SwiftUI and UIKit, with runtime white-label theming

If you've built white-label apps, tenant-branded apps or even just a SwiftUI rewrite of a UIKit app, you know the pain: colours hard-coded in three places, buttons that look slightly different on every screen, and a dark mode that only half works.

DesignSystemKit is my answer: a native design system for SwiftUI and UIKit, distributed with Swift Package Manager. One set of tokens, typography, components and icons, with themes you can switch at runtime for white-label apps, tenant branding and seasonal skins. Free, MIT licensed, and no third-party dependencies.

What's inside

Module What it gives you
DesignSystemCore Semantic colour tokens (light + dark), spacing, radius, border, elevation and motion scales, typography, DSTheme, ThemeStore with persistence, a tenant ThemeRegistry, a ThemeValidator that checks WCAG contrast, and field validation. No UI framework imports.
DesignSystemAssets Bundled Poppins fonts, a semantic icon catalog with SF Symbol fallback, and localized component strings (English, Spanish, German, Arabic, Hindi) with plurals.
DesignSystemSwiftUI .appTheme(...), @ThemeTokens, Dynamic Type text styles and components including buttons, text fields, checkbox, radio, toggle, picker, date field, card, badge, chip, banner, progress, dialog, empty state, skeleton/shimmer and more.
DesignSystemUIKit Dynamic UIColors and Dynamic Type UIFonts, theme observation for view controllers, window appearance binding, DSUIButton, DSUITextField, DSUICardView, DSUIBadge, DSUILabel.
DesignSystemPreview A browsable catalog of every token and component with a live theme switcher.

Requirements: iOS 16+, iPadOS 16+, Mac Catalyst 16+, macOS 13+ (Core, Assets and SwiftUI). Xcode 16 or later, Swift 6 toolchain.

Install

.package(url: "https://github.com/eeshuu/DesignSystemKit.git", from: "1.0.0"),
// …
.target(name: "App", dependencies: [
    .product(name: "DesignSystemSwiftUI", package: "DesignSystemKit"),
    .product(name: "DesignSystemUIKit", package: "DesignSystemKit"), // only if you use UIKit
]),
Enter fullscreen mode Exit fullscreen mode

Fonts, icons and translations ship with the package. Nothing has to be copied into your app.

SwiftUI in a few lines

@main
struct MyApp: App {
    @StateObject private var store = ThemeStore(persistence: UserDefaultsThemePersistence())
    init() { FontRegistry.registerBundledFonts() }

    var body: some Scene {
        WindowGroup { ContentView().appTheme(store: store) } // once, at the root
    }
}

struct ContentView: View {
    @ThemeTokens private var ds
    @State private var email = ""

    var body: some View {
        VStack(alignment: .leading, spacing: ds.spacing.lg) {
            DSText("Welcome back", style: .title)
            DSTextField("Email", text: $email, kind: .email,
                        validation: FieldValidator([.email("Enter a valid email")], isOptional: true).validate(email),
                        isRequired: true)
            DSButton("Continue", trailingIcon: .arrowRight, fullWidth: true) { }
        }
        .padding(ds.spacing.xl)
        .background(ds.colors.background)
    }
}
Enter fullscreen mode Exit fullscreen mode

UIKit gets the same tokens

final class ProfileViewController: UIViewController, DSThemeable {
    private let store: ThemeStore
    private var subscription: DSThemeSubscription?
    private let titleLabel = DSUILabel(style: .heading1, text: "Profile")
    private let save = DSUIButton(title: "Save", variant: .primary)

    override func viewDidLoad() {
        super.viewDidLoad()
        subscription = store.observe(self) // applies now and on every theme change
    }

    func apply(theme: DSTheme) {
        view.backgroundColor = .ds(.background, theme: theme)
        titleLabel.apply(theme: theme)
        save.apply(theme: theme)
    }
}
Enter fullscreen mode Exit fullscreen mode

Mixed apps work in both directions: DSHostingController themes SwiftUI inside UIKit, and the store is available via @Environment(\.dsThemeStore) for UIKit inside SwiftUI.

Runtime theming and white-label

store.selectTheme(id: "forest")         // brand theme
store.setAppearance(.dark)              // .system, .light or .dark
store.selectTheme(forTenant: "acme")    // tenant mapping you registered
Enter fullscreen mode Exit fullscreen mode

Four themes are built in (standard, forest, sunset and midnight). Create your own in code, or load one from JSON; anything the file leaves out falls back to the standard theme:

var brand = DSTheme.standard
brand.id = "acme"
brand.light[.primary] = "#D9480F"
brand.dark[.primary] = "#FFA94D"
brand.components.buttonShape = .pill
store.apply(brand) // validated, including WCAG contrast; refused if it has errors
Enter fullscreen mode Exit fullscreen mode
{ "id": "acme", "name": "Acme", "light": { "primary": "#D9480F" }, "components": { "buttonShape": "pill" } }
Enter fullscreen mode Exit fullscreen mode

That means a server can ship a tenant's branding as a small JSON file, and a bad palette with unreadable contrast gets rejected instead of shipped.

Accessibility and quality

The test suites cover token values, contrast of every built-in palette, theme JSON, persistence, font registration, localization and plurals, Dynamic Type scaling, UIKit observation, and snapshots in light and dark. The sample app's UI tests cover theme switching, tenants, form validation, accessibility identifiers, mixed navigation and the largest Dynamic Type size.

The repo includes a sample app (Examples/DesignSystemDemo) with a component catalog, a SwiftUI sign-up screen, a UIKit profile screen and a settings tab for theme, appearance and tenant switching.

My other Swift packages

  • SecureStorageKit: Keychain, Face ID and Secure Enclave storage
  • AppLoggerKit: privacy-first logging with OSLog, JSON Lines, redaction and export

github.com/eeshuu/DesignSystemKit: MIT licensed (bundled Poppins fonts under the SIL Open Font License). Stars, issues and feedback are very welcome.

Top comments (0)