SwiftUI CLI snapshotter that renders views to PNGs via the iOS Simulator — like Xcode Previews, but scriptable, CI-friendly, and deterministic.
- Two modes:
- Snippet‑mode: render any SwiftUI expression without touching your app code.
- Registry‑mode: register named scenes (like Previews) and render by id.
- Control device, light/dark, Dynamic Type, locale, orientation (visual), background, status bar, and wait.
- Status bar overrides are rendered directly in the PNG so the top strip reflects
--status-barvalues. - YAML batch for matrices.
- Pulls PNGs to host file system; clears simulator status bar overrides.
# List available simulators
ios-snap devices
# Render a simple view
ios-snap render \
--expr 'Text("Hello")' \
--imports 'SwiftUI' \
--device 'iPhone 15' \
--out ./screens/hello.png
- Requires macOS, Xcode (with iOS Simulator runtime), SwiftPM.
- Build from source:
git clone https://github.com/level-two/ios-snap.git
cd ios-snap
swift build -c release
cp .build/release/ios-snap /usr/local/bin/
Or invoke via swift run ios-snap … during development.
After the package has been built at least once, you can invoke the CLI through the bundled command plugin:
swift package ios-snap --help
swift package ios-snap render \
--expr 'Text("Hello")' \
--imports 'SwiftUI' \
--device 'iPhone 15' \
--out ./screens/plugin-hello.png
The plugin forwards arguments to the ios-snap binary and runs from the package root so relative paths resolve just like the standalone executable.
Simulator-driven commands (render, list, batch) require broader filesystem access; invoke the plugin with swift package --disable-sandbox ios-snap … (or granular --allow-reading-from-directory/--allow-writing-to-directory flags) when you need to touch the Simulator. Alternatively, run the compiled ios-snap binary directly.
- Snippet‑mode (zero integration):
ios-snap render \
--expr 'LocksScreen(viewModel: .preview)' \
--imports 'MyAppUI,MyAppMocks' \
--device 'iPhone 15 Pro' \
--appearance dark \
--status-bar 'time=09:41 wifi=3 cellular=4 battery=100' \
--out ./screens/locks-dark.png
- Registry‑mode (minimal integration):
// SnapshotScenes.swift in your UI module
import SwiftUI
import SnapshotKit
import MyAppUI
import MyAppMocks
@Snapshot("locks/default", size: .iPhone15Pro)
var locksDefault: some View { LocksScreen(viewModel: .preview) }ios-snap list
ios-snap render --scene 'locks/default' --device 'iPhone 15' --out ./screens/locks-default.png
# Demo registry scenes shipped with this repo
ios-snap list --device 'iPhone 15' \
--snippet Examples/Registry/Scenes.swift
ios-snap render --scene 'demo/hello' --device 'iPhone 15' \
--snippet Examples/Registry/Scenes.swift \
--out ./screens/demo-hello.png
Contents of Examples/Registry/Scenes.swift register scenes via the SnapshotRegistry API:
import SwiftUI
#if canImport(SnapshotKit)
import SnapshotKit
#endif
private func registerSnapshots() {
SnapshotRegistry.register("demo/hello", size: .iPhone15Pro) { DemoHelloView() }
SnapshotRegistry.register("demo/error", size: .iPhoneSE3) { DemoErrorView() }
}
func makeView() -> some View {
registerSnapshots()
return EmptyView()
}- Define scenes and variants in a YAML/JSON file (see
docs/BATCH_CONFIG.mdfor schema). - Relative paths inside the config resolve against the config file directory.
- Run all variants:
ios-snap batch --config Examples/Snapshots.yml
- Keep going after failures (exit code reflects first failure):
ios-snap batch --config Examples/Snapshots.yml --continue-on-error
- The bundled
Examples/Snapshots.ymlgenerates snippet and registry screenshots intoscreens/.
If your views live in a separate module (framework/package), you can direct ios-snap to build/link those dependencies before compiling the Runner:
Snippet example with a workspace scheme:
ios-snap render \
--snippet Snapshots/CheckoutFlow.swift \
--workspace MyApp.xcworkspace \
--dep-scheme CheckoutUI \
--device 'iPhone 15' \
--out screens/checkout.png
SwiftPM package example:
ios-snap render \
--snippet Snapshots/CheckoutFlow.swift \
--package ../Checkout \
--product CheckoutUI \
--device 'iPhone 15' \
--out screens/checkout.png
See Examples/ExternalDeps/README.md for ready-to-run demos that ship with the repository (framework + SwiftPM).
- Run
Scripts/selftest.shon a macOS host with Xcode and simulator runtimes installed to execute the verification matrix fromSPEC.md. - The script builds
ios-snapin release mode (unlessIOS_SNAP_BINis set) and writes outputs toscreens/selftest/alongside batch artefacts inscreens/. - Override defaults with
SELFTEST_DEVICE(target simulator name) orSELFTEST_OUT_DIR(output folder) as needed. - Inspect the console output to confirm dark mode, Dynamic Type, status bar overrides, batch runs, and registry renders succeed.
Package.swift
Sources/
ios-snap/ # CLI executable
SnapshotKit/ # Registry runtime
Templates/
Runner/ # Minimal SwiftUI runner Xcode project
Examples/
Registry/ # Sample registry declarations
ExternalDeps/ # Framework + SwiftPM demos for dependency flags
Snapshots.yml # Batch example
Scripts/
selftest.sh # End‑to‑end checks
docs/
README.md # Documentation index and guides
- See
docs/README.mdfor the full index. - Quick links:
docs/ARCHITECTURE.mddocs/CLI_REFERENCE.mddocs/BATCH_CONFIG.mddocs/CODING_STYLE.mddocs/TESTING.mddocs/CI.mddocs/TROUBLESHOOTING.mddocs/CONTRIBUTING.md- Root
AGENTS.mdfor agent workflow rules
MIT
Notes
- Products must build for
iphonesimulator; for Swift modules, enablingBUILD_LIBRARY_FOR_DISTRIBUTION=YESis recommended. - The Runner project is not modified; ios-snap injects search paths and linker flags at build time.