A Playbook-style gallery for SwiftUI: you register component states in code, then browse them as a website (and optionally a sheet in the app).
Nothing is scanned from disk. The board is whatever the running process registered.
Work in progress. SwiftBoard is pre-1.0 and actively developed: features are still being added, and a minor release may change an API. Pin a version and check the release notes before upgrading.
| Product | Use when |
|---|---|
| SwiftBoardServer | You have (or will write) your own design system |
| SwiftBoardKit | You want SwiftDS as a ready-made system and its catalog on the board |
Same Swift package URL. In Xcode you only link the product you need. Kit pulls SwiftDS; you do not copy a registrar file.
SwiftDS is a separate UI library. Fork it to change how controls look. Kit only wires its catalog into SwiftBoard.
There is no .xcodeproj: this is a Swift package. Either use the terminal, or open Package.swift (root) / Examples/Package.swift in Xcode and pick a scheme.
| What you want to check | Run | You'll see |
|---|---|---|
| It builds and tests pass (what CI runs) | swift test · Xcode: ⌘U |
All XCTest cases green |
| SwiftBoard alone (own design system) | swift run SwiftBoardDemo · Xcode: scheme SwiftBoardDemo, destination My Mac |
A small Mac window, and the site at http://127.0.0.1:8080 with fake DemoButton / DemoBadge / DemoCard components |
| SwiftBoard + SwiftDS on the Mac | cd Examples && swift run SwiftBoardKitDemo · Xcode: open Examples/Package.swift, scheme SwiftBoardKitDemo, My Mac |
Mac window with Open in Browser / Run iOS Demo, site with the SwiftDS catalog in macOS chrome |
| SwiftBoard + SwiftDS on iOS | Boot one Simulator, then ./Scripts/run-ios-demo.sh from the repo root |
The app opens on the Simulator with the in-app gallery, and also serves http://127.0.0.1:8080; open it in the Mac's browser. Snapshots use real iOS chrome |
When a demo says Running at http://127.0.0.1:8080, open that URL. That site is the product.
Things that trip you up:
- Both Mac demos are macOS-only (they import AppKit). In Xcode, a Simulator destination fails with
No such module 'AppKit'; switch to My Mac. For iOS, use the script: SwiftPM cannot build an installable iOS.appby itself, so the script assembles one. - One demo at a time. They all bind port 8080. The iOS demo ignores a failed start, so if a Mac demo is still up, the iOS app opens but serves nothing.
- Stopping. The site lives as long as the process:
Ctrl+Cforswift run, ■ in Xcode, or close the app on the Simulator (xcrun simctl terminate booted dev.luizmello.swiftboard.iosdemo). To find whoever holds the port:lsof -nP -iTCP:8080 -sTCP:LISTEN. - SwiftDS comes from GitHub, at the version pinned in
Package.resolved, not from a local SwiftDS checkout. A SwiftDS change shows up here only after it is tagged and you runswift package update. - Moved or renamed the folder? Builds fail with
PCH was compiled with module cache path …/missing required module 'SwiftShims'. Delete the stale caches and rebuild:rm -rf .build .build-ios-demo Examples/.build.
Requirements: iOS 17+ / macOS 14+, Xcode 15.3+ (Swift 5.10).
Xcode: File → Add Package Dependencies… → https://github.com/luizmellodev/SwiftBoard → Up to Next Major from 0.1.1.
Package.swift:
.package(url: "https://github.com/luizmellodev/SwiftBoard", from: "0.1.1")
// target:
.product(name: "SwiftBoardServer", package: "SwiftBoard")Link SwiftBoardServer only (not Kit).
// App.swift
WindowGroup {
ContentView() // your real UI — unchanged
#if DEBUG
.swiftBoard {
BoardScenarios.registerAll()
}
#endif
}On each component, a DEBUG extension (like a #Preview):
#if DEBUG
import SwiftBoardServer
extension AppButton: BoardScenarioProvider {
static func provideScenarios(into board: SwiftBoard) {
board.addScenarios(
category: "Actions",
component: "Button",
summary: "Primary tappable action.",
parameters: [ScenarioParameter(name: "title", type: "String")]
) {
Scenario("Primary", docs: ScenarioDocs(snippet: "AppButton(title: \"Continue\")")) {
AppButton(title: "Continue")
}
Scenario("Disabled") { AppButton(title: "Continue").disabled(true) }
}
}
}
#endifOne list of types:
#if DEBUG
import SwiftBoardServer
enum BoardScenarios {
@MainActor
static func registerAll() {
SwiftBoard.shared.addScenarios(from: [AppButton.self, AppBadge.self])
}
}
#endif⌘R → green SwiftBoard badge → sheet (in-app gallery) or copy the URL and open it on the Mac. Stop the app and localhost dies.
Existing app: same steps, one component at a time. On a physical iPhone, add NSLocalNetworkUsageDescription to Info.plist. The copied 127.0.0.1 link only works on the device itself (or from the Simulator's host Mac); from a Mac, open http://<iPhone-IP>:8080 instead (Settings → Wi-Fi → ⓘ shows the IP), with both on the same network.
Link SwiftBoardKit. Do not copy any registrar.
#if DEBUG
import SwiftBoardKit
.swiftBoard {
SwiftBoardKit.registerAll()
}
#endifUse DSButton (etc.) in your UI. You can still register your types in the same registerAll() after SwiftBoardKit.registerAll().
Localhost (http://127.0.0.1:8080) is a live server inside the DEBUG app. Designers only see it if that process is up. Reloading the browser after a code change is not enough: rebuild/run, then reload.
Hosted site (Vercel, Netlify, S3, GitHub Pages) is a static snapshot. Running the app does not update Vercel. You export a folder of HTML + PNGs and upload that folder.
The hosted site is a photograph of one build. New components or a new DS version do not appear until you export again and replace what is on the host.
-
Change the design system and registrations. Rebuild.
-
On a Mac (or iOS Simulator) — SwiftUI snapshots cannot be generated on Linux/Vercel — export:
try await StaticExporter.export(to: URL(fileURLWithPath: "./site"))
From this repo:
SWIFTBOARD_EXPORT=./site swift run SwiftBoardDemo
-
That folder is the whole site:
index.html,app.css,app.js,docs.html,manifest.json,snapshots/**/*.png. Upload all of it, not only the HTML. -
Point the host at that folder (Vercel: deploy the directory, or drag-and-drop). Designers open the public URL. No app running.
To ship 0.0.2 after 0.0.1: repeat 1–4. Overwrite the previous deploy. Old PNGs are not patched in place; each export is a full new tree.
Optional: a macOS CI job that runs the export and deploys the folder. A Linux Vercel build cannot call StaticExporter.
Without a server, .swiftBoard still shows BoardGallery in the sheet. BoardGallery alone (product SwiftBoard, no FlyingFox) is the same registry as live SwiftUI, not snapshots.
SwiftBoard does not discover views. Skip summary / parameters / ScenarioDocs and you still get named states and pictures.
Same category+component again appends states. Remove a component by deleting its registration and restarting — nothing is persisted.
Default: server is DEBUG-only. Release: SWIFTBOARD_ENABLE=1.
Network exposure. The server listens on every network interface, not only 127.0.0.1. That is what lets a Mac open the board served by a physical iPhone, but it also means anyone on the same Wi-Fi can browse your registered components and their snippets while the app runs. Avoid running it on untrusted networks (cafés, conferences). Custom actions (addAction) only run for requests from the device itself; other devices get 403.
Without .swiftBoard, you can still .task { register(); try await BoardServer().start() }.
Does adding SwiftBoard change my production UI? No. Link it, call it only in DEBUG.
Does SPM download SwiftDS if I only wanted Server? Resolving this package may fetch SwiftDS because Kit exists. Your app binary only includes SwiftDS if you link SwiftBoardKit (or SwiftDS yourself).
Does SwiftDS import SwiftBoard? No. Kit is the bridge.
Can I git clone a “Kit repo”? No. One package, two products.
Live-reload? No. Rebuild, run, reload the browser.