Skip to content
OddMagnetPublic

About

Render SwiftUI #Preview macros to PNG from the command line.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

suip

Render a SwiftUI #Preview to PNG from the command line, using the build products of a real iOS project.

The intended loop is simple: edit a view and its preview in your preferred editor, run suip, then inspect the resulting PNG. Once the application target has a Debug simulator build, suip can render the current preview without opening Xcode's preview canvas or rebuilding the application.

suip is editor-agnostic. It is intended for previewing SwiftUI work without depending on Xcode's preview canvas and as a foundation for future editor integrations.

Example output

A trail dashboard SwiftUI preview rendered by suip

Rendered by suip from the tracked ShowcasePreview.swift fixture.

Why this is difficult

A SwiftUI preview is not a standalone image description. Its body may depend on internal target types, file-scoped helpers, compilation conditions, package dependencies and code that only exists in the latest edit. It must also run inside an iOS environment rather than directly on macOS.

suip combines the edited source with the target's existing Debug build. It compiles a small per-edit dynamic library, loads that library into a reusable host inside an iOS Simulator, and snapshots the resulting SwiftUI view.

Architecture

suip has three cooperating pieces:

  1. Mac CLI - discovers the project and owning target, extracts the preview, invokes swiftc, manages caches and controls the simulator.
  2. Simulator host - loads the per-edit dynamic library with dlopen, calls its exported preview entry point and writes the rendered view to PNG.
  3. Shared contracts - JSON request and response messages plus a dynamic PreviewProtocol library that carries the SwiftUI view across the library boundary.

The application target remains responsible for compiling the wider project and its dependencies. suip borrows those build products while recompiling the source needed to reflect the current preview edit.

Requirements

  • An Apple silicon Mac running macOS 13 or newer
  • A Swift 6.2 or newer toolchain to build suip, as included with Xcode 26 or newer
  • Xcode 16 or newer selected when rendering; Xcode 26 satisfies both toolchain requirements
  • An iOS project or Swift package that declares an iOS deployment target
  • A Debug build of the selected target for an arm64 iOS Simulator
  • A booted iOS Simulator

For an Xcode target, previews also require its Debug dynamic library. Targets with ENABLE_DEBUG_DYLIB = NO or MERGED_BINARY_TYPE are not supported.

Building suip

From a checkout of this repository:

swift build -c release

The executable is written to .build/release/suip.

The suip checkout and the application being rendered can live in separate directories. Before changing to the application directory, save the executable's absolute path in the current shell:

SUIP="$(pwd)/.build/release/suip"

Usage

Build the application target for an iOS Simulator, boot a simulator, and run suip from the application's project root:

cd /path/to/YourApp
"$SUIP" render YourApp/ProfileCard.swift

render is the default subcommand and may be omitted. If the source belongs to several targets, select one explicitly:

"$SUIP" render YourApp/ProfileCard.swift \
  --target YourApp

A successful render prints the resolved target information followed by the PNG path:

target: YourApp
module: YourApp
ios: 17.0
sdk: /path/to/iPhoneSimulator.sdk
/Users/you/Library/Caches/suip/<project-hash>/dylibs/<content-hash>/preview.png

Run suip render --help for the current command syntax.

Supported projects

suip recognizes these project markers at the top level of the current directory:

  • An .xcworkspace
  • An .xcodeproj
  • A Package.swift

Workspaces may reference Xcode projects and Swift packages. Source ownership is resolved from target membership, including Xcode filesystem-synchronized groups. Use --target when more than one target owns the source file.

Project discovery does not walk up through parent directories. Run suip from the directory containing the project marker, and pass a source file below that directory.

Swift packages

suip currently expects SwiftPM's native-build layout:

.build/arm64-apple-ios-simulator/debug/

Swift 6.4 and later use a different swiftbuild layout by default. Until suip supports that layout, build standalone packages with the native build system:

swift build \
  --triple arm64-apple-ios-simulator \
  --sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \
  --build-system native

SwiftPM currently prints a deprecation warning for the native build-system option. The command still produces the layout suip requires while support for the default layout is being developed.

Local Swift packages reached through an Xcode-rooted project are not yet supported.

Status

suip is a work in progress. The end-to-end rendering pipeline works, but the command-line and integration contracts are not yet stable.

Current limitations include:

  • A source file must contain exactly one #Preview.
  • Sibling extensions generally work. If copied extensions from several files import modules that define the same name, compilation can fail with an ambiguous-name diagnostic.
  • suip cannot detect when edits in other target files are newer than the Debug build. Rebuild the application target before rendering a preview that depends on those edits.
  • After rebuilding a target, the long-running simulator host can retain the previous target image. Stop it with xcrun simctl terminate booted dev.oddmagnet.suip, then run suip again.
  • Swift 6.4 and later's default SwiftPM swiftbuild layout is unsupported.
  • Local packages inside Xcode-rooted projects are unsupported.
  • Output is human-readable only. Structured JSON output and distinct failure-category exit codes are not implemented.
  • Custom size, scale, appearance and locale traits are not implemented.

Verification

The rendering pipeline is exercised end to end against focused fixture projects and real iOS project builds. This covers project discovery, target selection, dynamic library compilation, dependency scanning, simulator communication, caching, source slicing and sibling extensions.

The tracked fixtures/App project provides a reproducible end-to-end check. With an iOS Simulator already booted, run these commands from the suip repository root:

swift build
xcodebuild \
  -project fixtures/App/MinimalApp.xcodeproj \
  -scheme MinimalApp \
  -destination 'generic/platform=iOS Simulator' \
  -configuration Debug \
  build
(
  cd fixtures/App
  ../../.build/debug/suip render MinimalApp/ContentView.swift
)

There is not yet a conventional test target. It was deferred while the pipeline was being established and remains part of the project cleanup work rather than a substitute for end-to-end verification.

AI assistance

Project conventions and the rules for AI-assisted work are documented in AGENTS.md.

The product code is handwritten. AI assistance is used to read and review code, explain behavior, help plan work, surface tradeoffs and summarize maintainer-written changes. It does not write product code; test fixtures and throwaway spikes are exceptions. The maintainer decides what enters the repository, and commits summarized by AI are marked accordingly.

Roadmap

  1. Isolate generated sources from different sibling files.
  2. Select among multiple previews in one source file.
  3. Report when borrowed build products are stale.
  4. Refresh the simulator host after a target rebuild.
  5. Support SwiftPM's current default build layout.
  6. Discover local packages inside Xcode-rooted projects.
  7. Add structured output and distinguishable exit codes.
  8. Build a Neovim integration.
  9. Add custom rendering traits after the initial editor integration.

About

Render SwiftUI #Preview macros to PNG from the command line.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages