MCP server by hmSchuller
xcpv
xcpv is a Swift CLI that turns a Swift source file inside a Swift package into a persistent, reusable iOS preview host app.
It does not use Xcode's inline preview canvas. Instead, it:
- finds the owning Swift package,
- resolves the SwiftPM target and library product,
- parses preview declarations from the selected file,
- creates or reuses a cached iOS host project,
- rewrites only
GeneratedPreviewHost.swift, and - either opens the cached project, runs a full simulator build with
xcodebuild, or refreshes the Simulator via a bridge-onlyswiftcfast path when possible.
Status
v1 focuses on:
- iOS only
- Swift packages only
- common
#Previewblocks - basic
PreviewProvidersupport - persistent XcodeGen-generated host reuse
Requirements
- macOS
- Xcode /
xcodebuild - Swift 6.2+
- XcodeGen for first-time host generation
Install XcodeGen:
brew install xcodegen
Build
swift build
Usage
xcpv open <file>
xcpv watch <file>
xcpv run <file>
xcpv inspect <file>
xcpv clean
xcpv open <file>
- resolves package / target / product
- creates or reuses the host project
- rewrites
GeneratedPreviewHost.swift - opens the cached Xcode project
Example:
swift run xcpv open Sources/FeatureUI/ContentView.swift
xcpv watch <file>
Prepares the cached host project, boots an iOS Simulator, launches PreviewHostApp, and keeps it updated from the CLI.
Behavior:
- selects and boots an iOS Simulator device
- performs an initial build + install + launch of
PreviewHostApp - watches the selected source file
- regenerates only
GeneratedPreviewHost.swift - when only the generated bridge changed, rebuilds
PreviewContent.dylibdirectly withswiftc - otherwise rebuilds
PreviewHostAppwith watch-optimizedxcodebuildflags - coalesces rapid edits by canceling an in-flight rebuild and restarting from the latest file state
- skips simulator updates when the rebuilt app binary or dylib is unchanged
- otherwise reinstalls or syncs the simulator app and relaunches
PreviewHostApp - can optionally print per-phase timings, compiler incremental diagnostics, and use an experimental fast-install path
- does not open Xcode
swift run xcpv watch Sources/FeatureUI/ContentView.swift
Useful watch flags:
--timingsprints bridge/build/update/relaunch timings for each rebuild--show-incrementalprints the Xcode build timing summary plus any available Swift incremental compilation diagnostics--fast-installuses an experimental simulator bundle sync path to avoidsimctl installon rebuilds when possible
Example:
swift run xcpv watch --timings --show-incremental --fast-install Sources/FeatureUI/ContentView.swift
xcpv run <file>
Attempts to:
- boot an iOS Simulator
- build the cached host app
- install and launch it
If that flow fails, xcpv falls back to opening the Xcode project.
swift run xcpv run Sources/FeatureUI/ContentView.swift
xcpv inspect <file>
Prints:
- package root
- target
- product
- transitive local target dependencies
- detected previews
- host project path
- cache key
swift run xcpv inspect Sources/FeatureUI/ContentView.swift
xcpv clean
Removes the cache directory.
swift run xcpv clean
Cache layout
Cached hosts live under:
~/Library/Caches/xcpv/
hosts/
<hash>/
project.yml
PreviewHost.xcodeproj
App/
PreviewHostApp.swift
GeneratedPreviewHost.swift
metadata.json
Cache identity
The host cache key is derived from:
- package root
- selected library product
- platform (
iOS)
Reuse rules
The full Xcode project is regenerated only when:
- the host project is missing,
- metadata is missing or incompatible,
- the host schema version changes.
The frequently-changing file is:
App/GeneratedPreviewHost.swift
That file is rewritten deterministically and only when content changes.
How it works
1. Package discovery
xcpv walks upward from the selected Swift file until it finds Package.swift.
2. SwiftPM introspection
It runs:
swift package dump-package
and decodes the manifest JSON to resolve:
- package name
- targets
- products
- local target dependencies
3. Target and product resolution
The tool determines which target owns the selected file, then chooses a direct library product containing that target.
If a target is only reachable transitively through another product, xcpv reports that clearly instead of guessing.
4. Preview parsing
swift-syntax is used to inspect the file and detect:
#PreviewPreviewProvider
For multiple previews, the generated host renders them in a TabView.
5. Host project generation
A cached XcodeGen spec produces a minimal iOS app target:
- app target:
PreviewHostApp - project name:
PreviewHost - stable app entry source:
PreviewHostApp.swift - generated bridge source:
GeneratedPreviewHost.swift
The host app depends on the local Swift package and links the resolved library product.
6. Simulator refresh + watch coordination
At runtime, PreviewHostApp loads preview content from PreviewContent.dylib, which is compiled from GeneratedPreviewHost.swift.
During watch, xcpv chooses the fastest refresh path it can:
- bridge-only edit: compile just
PreviewContent.dylibwithswiftcand sync it into the installed simulator app - library/source edit or failed fast path: fall back to a full
xcodebuildand simulator app update
The watch loop debounces file changes, coalesces overlapping rebuild requests, and cancels superseded subprocesses so the latest edit wins.
Architecture
Sources/
XcpvCLI/
main.swift
CLI/
XcpvCommand.swift
Commands.swift
Core/
Models/
PackageDiscovery/
SwiftPM/
Parsing/
Generation/
Hosting/
Watching/
Support/
Key modules:
- CLI: ArgumentParser commands
- PackageDiscovery:
Package.swiftlookup - SwiftPM:
dump-packageloading, target/product resolution - Parsing: preview extraction with
swift-syntax - Generation: deterministic bridge generation + file writing
- Hosting: cache identity, XcodeGen spec, reusable host project, direct bridge dylib compilation, watch-optimized simulator build / install / relaunch, diagnostics, and experimental fast-install sync
- Watching: debounced single-file watching plus rebuild coordination that coalesces changes and cancels superseded rebuilds
- Support: logging, subprocess execution, path helpers, and cancellation primitives
Limitations
Current MVP limitations:
- only iOS host generation is implemented
- first-time host generation requires
xcodegeninPATH - preview parsing is optimized for common
#Preview { ... }forms - complex macro argument edge cases are not exhaustively handled yet
- generated bridge code imports the resolved library product; previews that rely on non-exported/internal symbols may not compile from the host app module
PreviewProviderextraction supports the commonstatic var previews: some View { ... }pattern best
Tips for large packages
xcpv resolves the watched file to its owning SwiftPM target and then builds the corresponding library product plus its transitive target dependencies.
If your preview file lives in a large target like MyLibraryCore, changing even a single preview string still pays for:
- incremental recompilation inside that target
- Swift module emission for that target
- relinking the preview host app
- simulator update / relaunch work
A practical way to reduce that cost is to move preview-only files into a tiny preview-support target that depends on the main library target.
Conceptually:
products: [
.library(name: "MyLibraryCore", targets: ["MyLibraryCore"]),
.library(name: "MyLibraryPreviewSupport", targets: ["MyLibraryPreviewSupport"])
],
targets: [
.target(name: "MyLibraryCore", dependencies: ["MyLibraryInternal", "MyLibraryThemes"]),
.target(name: "MyLibraryPreviewSupport", dependencies: ["MyLibraryCore"])
]
Then place preview-facing fixtures and preview wrapper views in MyLibraryPreviewSupport and point xcpv watch at files in that target. That keeps the watched build unit smaller than the main library target.
Tradeoffs:
- the preview-support target can only use APIs visible across target boundaries (
publicorpackageas appropriate) - previews that rely heavily on
internalimplementation details may need small adapter APIs or helper types
Tests
Included tests cover:
- package discovery
dump-packagemanifest decoding- target resolution
- product selection
- preview detection
- bridge generation
- cache reuse behavior
xcodebuildrequest generation- simulator prepare / launch sequencing and bridge dylib fast-path refreshes
- process cancellation and watch rebuild coordination
- file watcher change detection
Run them with:
swift test
License
MIT. See LICENSE.