CLI tool
BarredEwe/Prefire avatar
BarredEwe/Prefire

Prefire: turning SwiftUI previews into snapshot tests and a component catalogue

🔥 A library based on Xcode Preview, for easy generation: Playbook view, Snapshot and Accessibility tests. SwiftUI and UIKit supported!

525 stars41 forksSwiftApache-2.0

At a glance

What is it?
Prefire is an Xcode plugin and command line tool that reads your #Preview blocks and generates snapshot tests, a playbook view of every component and a set of visual flows from them. Its argument is that the previews you already write are the tests you should have, and it caches aggressively so the generation stays fast.
Who is it for?
Adopt Prefire if you already write SwiftUI previews and want snapshot tests and a component catalogue generated from them, since that is the whole premise and it removes the usual duplication between a preview and a test. Do not expect it to replace visual review, since the snapshots it produces still need somebody to look at the diff when one changes.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The premise: the previews you wrote are the tests you should have

Prefire is a library based on Xcode Preview for easy generation of a Playbook view, snapshot tests and accessibility tests, with SwiftUI and UIKit supported. The homepage is prefire.ru, the primary language is Swift, and the licence is Apache-2.0 with the last push on 2026-10-20.

The transformation it performs is stated in one line: Prefire transforms your #Preview blocks into snapshot tests, Playbook views, visual flows with states and user stories, and living documentation that is fully automated.

That last category is the ambition. The normal state of affairs in a SwiftUI codebase is that previews live in the source file, tests live somewhere else, and the two drift apart until a preview shows a state nobody has ever asserted on. Prefire's argument is that if the test is generated from the preview, the drift cannot happen, and the README's stated benefit is keeping previews and tests always in sync.

The key features list gives the mechanics. Smart preview parsing covers #Preview and @Previewable. Snapshot testing means automatic test generation from previews, for iOS, tvOS and macOS. The Playbook view is an auto-generated interactive component catalogue for iOS. It is flow-aware, so you can build user stories from multiple preview steps. There is UIKit and AppKit support for UIView, UIViewController, NSView and NSViewController. It works through SPM, Xcode build phases or CI, with both a Swift Package Manager plugin and Xcode plugins. Caching is fingerprint based, with AST and body caching that avoids redundant work. And output is produced with Stencil templates, so you can customise it with your own templates.

The stated reasons for using it are a mixture of time saving, staying consistent, improving quality by catching visual regressions before users do, and boosting collaboration by sharing living documentation with your team. That last one is the real differentiator: the artefact is something you can put in front of people who do not write Swift.

The recent releases are 6.2.0 on 2026-10-19, 6.1.0 on 2026-10-12 and 6.0.0 on 2026-10-08, so the version line is moving quickly and the project is clearly in active use.

Getting set up: a package dependency, a plugin, and a preview

The quick start has three steps, and the first is the dependency declaration in Package.swift. The Prefire package comes from the repository itself, and the snapshot testing library comes from Point-Free:

swift
// Package.swift
dependencies: [
    .package(url: "https://github.com/BarredEwe/Prefire.git", from: "5.4.0")
    .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.0"),
],
.testTarget(
    dependencies: [
      .product(name: "Prefire", package: "Prefire"),
      .product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
    ],
    plugins: [
        .plugin(name: "PrefireTestsPlugin", package: "Prefire")
    ]
)

The plugin is what wires the generation into the build, so the tool runs when the test target builds rather than when you remember to run a command.

Then you write a preview, which for most people means the SwiftUI preview macro and nothing else:

swift
#Preview {
    Button("Submit")
}

And then you run the test target. The README is emphatic that Prefire auto-generates the snapshots based on your previews, and adds the useful detail that if your test target is empty, Prefire will still generate files and snapshot code during the build. That matters because the first thing that happens to a new user is they add the plugin, have no tests yet, and wonder whether anything happened.

The installation section lists five supported routes, so the tool can be adopted at whatever level of commitment suits the project. There is the SPM plugin used in Package.swift, an Xcode build tool plugin, a command line install with brew install prefire, Mint as a beta option with mint install BarredEwe/Prefire, and GitHub Actions or CI. The detailed setup is in the installation guide in the Documentation directory, and there is an example project in the repository under Example.

The Mint route has an interesting implementation detail. The installed prefire wrapper ships the PrefireBinary artifactbundle as an embedded SwiftPM resource, so it is self-contained and no external binary path is required, and to override the path of the embedded binary, for example for local development against an in-progress build, you set PREFIRE_BINARY_PATH to the absolute path of the prefire executable. Shipping the binary inside the wrapper is what makes the other three install routes work, since the plugin and the build phase all need a prefire executable somewhere to call.

How the generation works, and why the caching matters

The how it works section describes four steps, and the second one is the interesting one.

First, it parses all source files, finding all #Preview and PreviewProvider blocks, and it supports the modifiers.prefireEnabled() and.prefireIgnored().

Second, it caches Types and PreviewBodies, based on the file modification date plus a SHA-256 of the inputs, and this avoids re-parsing if nothing changed. That last point is what makes the tool usable on a large project. A SwiftUI codebase can have thousands of previews, and parsing every one of them on every build would be slower than the tests it generates. Caching on a content hash rather than on a timestamp alone means the cache stays correct when a file is edited and reverted, and the modification date check is a fast path on top of it.

Third, it generates snapshot tests, using Stencil templates and respecting the.prefire.yml configuration. Stencil is a template engine, and it is the reason the output is customisable: the generated file is a template rendered with your preview models, and you can supply your own template rather than patching the generated code after the fact.

Fourth, it generates the Playbook view, grouping by UserStory and State, and outputting PreviewModels.generated.swift.

The grouping is the design idea behind the catalogue. If previews are grouped by a state they represent and a story they belong to, then a folder of previews becomes a navigable set of screens with a coherent shape, rather than a flat list of unrelated views. In practice that means selecting a user story in the Playbook shows the sequence of states that make it up.

The repository layout follows the same split. There is a Sources/ directory for the library, a PrefireExecutable/ directory for the command line tool, a Plugins/ directory for the build plugins, a Binaries/ directory holding the PrefireBinary artifactbundle, a PrefireSnapshotTests/ directory, a Tests/ directory, an Example/ directory with a full Xcode project, a Documentation/ directory and a Makefile.

Opting in, and the three ways to control what gets generated

There are three mechanisms for controlling what Prefire picks up, and they cover different situations.

The first is the PrefireProvider protocol, which is how you mark a preview explicitly:

swift
struct Text_Previews: PreviewProvider, PrefireProvider {
    static var previews: some View { ... }
}

The second is that if you use the #Preview macro, Prefire will automatically find it. So for most SwiftUI code the protocol is not needed at all, and the macro is the default path.

The third is the opt-out modifier, for a preview you do not want tested:

swift
#Preview {
    Text("")
        .prefireIgnored()
}

And there is a fourth case that is worth understanding because it changes the default behaviour of the whole project. If you want to disable the automatic collection of all previews, use the setting preview_default_enabled: false in the configuration file, and then to include a preview in the test you have to call the .prefireEnabled() modifier on it:

swift
#Preview {
    Text("")
        .prefireEnabled()
}

So the model is opt-in by default, and the .prefire.yml setting flips it to opt-out. That is a sensible default for a codebase that already has hundreds of previews, since a plugin that suddenly generates a test for every preview in the repository would produce a very large diff on first run.

The configuration file is called .prefire.yml and it lives at the root of the project, and the example project in the repository has one. The same file is where the template selection and the other settings would go, since the README notes that generation respects the .prefire.yml configuration.

The Playbook view, and the snapshot API

The Playbook view is the interactive catalogue, and using it is a matter of placing one view in your app.

swift
import Prefire 

struct ContentView: View {
    var body: some View {
        PlaybookView(isComponent: true, previewModels: PreviewModels.models)
    }
}

The isComponent flag is the only control, and it changes what you see. If you want a list of all the views, use isComponent: true, and if you want to sort by user story, use isComponent: false. That is the catalogue versus the flow distinction, and it is a nice touch that a single flag switches between browsing components and walking through stories.

The snapshot API is where the interesting engineering is. The first form takes a delay and two precision values:

swift
    .snapshot(delay: 0.3, precision: 0.95, perceptualPrecision: 0.98)

The second waits for the preview to settle instead of guessing a delay:

swift
    .snapshot(waitForIdle: true)
    .snapshot(waitForIdle: true, timeout: 2.0)

The documentation explains the mechanism: Prefire renders the preview, compares the frames it would capture every 50 ms, and takes the snapshot once nothing changed for 300 ms. A preview that never settles fails with a message instead of hanging, after the timeout, which is 5 seconds by default.

That is a well-designed answer to the problem that makes most snapshot testing of SwiftUI views annoying, which is that you have to guess how long to wait for an animation or an async load to finish. Guessing a delay is flaky by construction, because a slow CI machine and a fast laptop need different values, and a fixed value that works locally is a test that fails in CI. Comparing frames until they stop changing is content based rather than time based, and the two constants, 50 ms and 300 ms, are the polling interval and the stability window.

The failure mode is handled rather than ignored: a preview that never settles fails with a message instead of hanging.

Two caveats are stated and they are both about what the tool can see. Put the modifier on content that is there from the first render, since one inside content that only shows up later, such as an if isLoaded branch or a List row, is not seen. And waiting for idle only sees what is drawn, so a preview that shows the same frame for longer while it loads, or runs a Core Animation animation, is not recognised as busy.

The build tooling behind it

The Makefile in the repository root shows how the project itself is developed, and it is a good indicator of the care taken with the build.

The file opens with a comment saying that these are the Prefire development tasks and that running make or make help lists the available targets. The shell is set to /bin/bash with strict flags, .DELETE_ON_ERROR is declared so a failed target removes its output rather than leaving a stale artefact behind, and the default goal is help rather than a build, which is a polite choice for a repository someone else will clone.

The version handling is worth noting. The version is read out of a Swift source file rather than being written in two places, with a sed expression pulling the string value out of a static let in the Version command, and the artefact bundle directory is then built from that version. So the binary, the bundle and the release all agree on the version by construction.

The build destination is deliberately unversioned, so the build uses whichever iOS Simulator SDK the selected Xcode provides, and the make variable for it can be overridden for other platforms. The simulator for the test target is picked by UDID rather than by device name, because device names differ between Xcode versions, which is a detail that saves a broken script every time Apple renames a simulator.

The slim-simulator target is the most unusual piece. It turns off a long list of simulator services on CI: widgets, Siri, Spotlight, iCloud, the stores, health and photos, among others, because a freshly booted simulator starts all of them and the tests do not use them. The list is the default profile of an existing tool called simslim, which is a sensible thing to lean on rather than maintain a bespoke list of launch daemons to disable.

The repository also has a Binaries/ directory holding the PrefireBinary artifactbundle, which is the same artefact the Mint wrapper embeds, and a Documentation/ directory with the installation guide and a templates guide, and an Example/ project with its own .prefire.yml, a Package.resolved and a PrefireExampleTests directory.

Editorial conclusion

Adopt Prefire if you already write SwiftUI previews and want snapshot tests and a component catalogue generated from them, since that is the whole premise and it removes the usual duplication between a preview and a test. Do not expect it to replace visual review, since the snapshots it produces still need somebody to look at the diff when one changes. Before you rely on the generated tests, read the caching section, because the cache is keyed on file modification date and a SHA-256 of the inputs, and note that the playground and catalogue are generated by Stencil templates you can replace with your own.

Frequently asked questions

What does Prefire do with my SwiftUI previews?

It transforms your #Preview blocks into snapshot tests, Playbook views, visual flows with states and user stories, and living documentation. The stated benefit is that previews and tests stay in sync, because the test is generated from the preview rather than written separately.

How do I install Prefire in a Swift project?

Add the Prefire package and swift-snapshot-testing to Package.swift, add the PrefireTestsPlugin to the test target, and run the test target. It also installs as an SPM plugin, an Xcode build tool plugin, a CLI with brew install prefire, or via Mint with mint install BarredEwe/Prefire, and it can run in GitHub Actions or CI.

How do I stop Prefire from generating a test for a preview?

Add the .prefireIgnored() modifier to that preview. To change the project-wide default so only explicitly marked previews are collected, set preview_default_enabled to false in the .prefire.yml configuration and then mark the ones you want with .prefireEnabled().

What does the waitForIdle snapshot option do?

It waits until the preview stops changing rather than guessing a delay. Prefire renders the preview, compares the frames it would capture every 50 ms, and takes the snapshot once nothing changed for 300 ms, and a preview that never settles fails with a message after the timeout, which is 5 seconds by default.

Can I customise the code that Prefire generates?

Yes. Test and playbook generation use Stencil templates, and the documentation has a templates guide for writing your own. Generation also respects the .prefire.yml configuration file at the project root, which is where settings such as preview_default_enabled live.

Official sources

  1. BarredEwe/Prefire on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/barredewe-prefire.svg)](https://hysenlabs.com/projects/barredewe-prefire)

Was this useful?