# SnapshotPreviews turns Xcode previews into snapshot images without test code

> SnapshotPreviews is a MIT licensed Swift package that discovers the previews in your app and renders each one as a PNG during an XCTest run, with no test methods to write, then writes PNGs plus JSON sidecars to a directory for upload to Sentry Snapshots. It supports SwiftUI and UIKit previews across iOS, macOS, watchOS, tvOS and visionOS, and version 0.20.0 was published on 2026-10-08.

**getsentry/SnapshotPreviews** — 📸 Automatic snapshots from Xcode previews. Supports UIKit/AppKit/SwiftUI on iOS/macOS/watchOS/visionOS/tvOS. Browse previews in-app with the Preview Gallery, or save them to PNGs with an XCTest

- Repository: https://github.com/getsentry/SnapshotPreviews
- Website: https://docs.sentry.io/product/snapshots/
- Stars: 631 · Forks: 32
- Language: Swift
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/getsentry-snapshotpreviews

## Previews become test cases that you do not write

SnapshotPreviews is a Swift package for turning Xcode previews into snapshot images. The pitch is that you get snapshot images from your previews with zero test code, and the exported images can go to Sentry Snapshots or any other visual diffing service.

The mechanism is discovery rather than enumeration. When the package starts, it finds the previews in your app, whether they are written with a PreviewProvider or with the #Preview macro, and it adds one test function per discovered preview at runtime. Your own test class contains no test methods at all.

Coverage is broad by design. The README states that it works with SwiftUI and UIKit previews, on all Apple platforms, naming iOS, macOS, watchOS, tvOS and visionOS. UIKit previews are the harder half of that, since a UIKit preview renders a view controller rather than a SwiftUI view, and a package that handles both is doing more than wrapping one API.

The package installs as a Swift Package Manager dependency, with the repository URL given directly. It ships two products with different link targets, and that split is the first thing to understand. The SnapshottingTests product is linked to your XCTest target, since it needs the test runner. The SnapshotPreferences product is linked to your app target, and it exists so you can customize per-preview rendering such as precision and layout from inside the view code itself.

The default behaviour is that each rendered preview is attached to the XCTest results bundle as a PNG, so the images show up in Xcode alongside the test run without any export step.

## One test class with four optional filters

The test class inherits from SnapshotTest, and the example in the README is the entire file:

```swift
import SnapshottingTests

class DemoAppPreviewTest: SnapshotTest {

  // Optional: return preview type names like "MyApp.MyView_Previews" to render only a subset.
  override class func snapshotPreviews() -> [String]? {
    return nil
  }

  // Optional: exclude specific previews from rendering.
  override class func excludedSnapshotPreviews() -> [String]? {
    return nil
  }
}
```

Two filters are visible there. snapshotPreviews returns the preview type names to render, so returning nil renders everything, and excludedSnapshotPreviews removes specific ones. The names are the preview type names as they appear in the code, such as MyApp.MyView_Previews.

Two more filters exist for the case where an app links several frameworks, and the README describes scoping discovery to modules:

```swift
// Only snapshot previews from these modules
override class func snapshotPreviewModules() -> [String]? { ["MyFeatureModule"] }

// Skip previews from these modules
override class func excludedSnapshotPreviewModules() -> [String]? { ["LegacyModule"] }
```

That pair matters in practice for a modular codebase, where the alternative is rendering every preview in every module on every run.

One naming detail will affect how you organise your files. The README notes that preview macros produce snapshot names based on file path and display name, for example MyModule/MyFile.swift:Display Name, whereas a PreviewProvider is identified by its type name. Two previews in the same file with different display names therefore get distinct snapshot names, and the path is part of the name, so moving a file changes the name of its snapshots.

## Exporting to a directory is one environment variable

For CI use, the images have to leave the results bundle. The README describes the flow in two steps, with xcodebuild test writing PNGs and JSON sidecars to a directory, and sentry-cli build snapshots uploading that directory afterwards.

The export is controlled by an environment variable on the test invocation:

```bash
TEST_RUNNER_SNAPSHOTS_EXPORT_DIR="$PWD/snapshot-images" \
xcodebuild test \
  -scheme MyApp \
  -sdk iphonesimulator \
  -destination 'platform=iOS Simulator,name=iPhone 15 Pro'
```

When that variable is set, SnapshotTest writes the images directly to the directory instead of attaching them to the .xcresult bundle. The two modes are mutually exclusive in the sense that the export mode replaces the attachment behaviour rather than adding to it.

The TEST_RUNNER_ prefix is not decoration. The README explains that this is how Xcode forwards an environment variable from xcodebuild into the test runner process, and that inside the runner the package reads the variable without the prefix. Anyone setting this by hand in a scheme or a CI job needs to know that the two names are not interchangeable, since inside the runner the unprefixed name is what is read.

The command in the example is otherwise an ordinary simulator test run, with a scheme, an SDK and a destination naming a specific device. Nothing about the snapshot machinery changes how the tests are launched, which is what makes it easy to add to an existing pipeline.

## The JSON sidecar is where grouping and thresholds live

For every rendered preview, two files are written. The first is the PNG, the rendered preview image. The second is a JSON sidecar, and the README describes it as the metadata used by Sentry Snapshots.

The sidecar carries five fields. display_name is the snapshot name shown in Sentry, generated from the preview name, file path and module so exported filenames stay stable and unambiguous. group is the grouping key Sentry uses to organise related snapshots, generated from the same inputs. diff_threshold is the allowed visual difference for that snapshot. tags are optional key-value pairs for filtering and grouping in Sentry. context holds supporting metadata.

The context field is the richest of them, and the README enumerates what it can carry: test name, simulator info, orientation, colour scheme, source line, preview attributes and any custom context you add. The package adds a set of keys by default when the values are available, among them test_name, accessibility_enabled, simulator.device_name, simulator.model_identifier, preview.index, preview.display_name, preview.container_type_name, preview.container_display_name, preview.preferred_color_scheme, preview.orientation and preview.line.

That default set is a design decision about what makes a visual diff actionable. A snapshot that differs is much easier to triage when you know which simulator produced it, which colour scheme it rendered in and which preview it came from, and the fact that the package records those by default means you do not have to instrument each preview to get them.

The diff threshold is the mechanism for tolerating legitimate variation, and its presence at the snapshot level rather than only globally is what allows one animated or image-heavy preview to have a looser tolerance than the rest.

## Per-preview control comes from the SnapshotPreferences product

The SnapshotPreferences product is the one linked to your app target rather than your test target, and it is what lets a preview describe its own metadata.

Three modifiers are shown in the README, applied to a preview body:

```swift
import SnapshotPreferences

#Preview("Map") {
  MapPreview()
    .snapshotTags(["screen": "map"])
    .snapshotAdditionalContext(["fixture": "city-route"])
    .snapshotDiffThreshold(0.05)
}
```

snapshotTags sets the tags used for filtering and grouping in Sentry. snapshotAdditionalContext adds custom fields to the context. snapshotDiffThreshold sets the allowed visual difference for that specific preview, and 0.05 allows up to a five percent difference for that snapshot.

The merge semantics of the custom context are worth reading carefully, because they have a consequence. Custom context is shallow-merged into the generated context, so a custom key such as test_name replaces the generated value rather than being added alongside it. That is a trap in the other direction too: because the merge is shallow, a nested object in the generated context is replaced wholesale, not merged key by key.

Supported custom values are strings, numbers, booleans and nested objects. The convention of putting the metadata next to the view it describes is the point of the design, since a preview that renders a map fixture can declare the fixture name in the same file, and a reader of the diff later sees where the difference came from.

## A second environment variable supports selective testing

There is a second environment variable, and it does something different. TEST_RUNNER_SNAPSHOTS_ALL_IMAGE_NAMES_FILE writes all discovered logical .png image names to the given file and then returns without rendering previews. The README says it is used to support selective testing workflows.

The workflow it enables is a two phase CI run. First, a cheap pass discovers the names of every snapshot that would be produced and writes them to a file without rendering anything. Second, a test run renders only the subset that is relevant, which is the set of previews touched by the change under test.

That matters when the previews are expensive. Rendering every preview in a large app on every push is slow, and the cost grows with the app rather than with the change. Listing the names first turns the expensive phase into something you can narrow, which is the standard approach for a large visual test suite.

The two modes are mutually exclusive, and the README is explicit about the consequence. If the names file variable is set, the package writes image names only and does not render or export snapshot images. Setting both by accident gives you a file of names and no images, which is a confusing failure, so the precedence is worth remembering when you write the CI script.

There is also a deliberate omission in the exporter. No Xcode code coverage data, in the form of .profraw or .profdata files, is written by the exporter, only the PNGs and the sidecars. If you need coverage from the same test run, the README says to enable it on the scheme as usual, since coverage output goes to the .xcresult bundle independently.

## Uploading to Sentry, and where the package sits

The upload step uses sentry-cli, and the README gives the command as sentry-cli build snapshots against the directory the test run wrote. The visual diff interface on the Sentry side is what the grouping, tags and thresholds feed into, which is why the sidecar format carries those fields rather than leaving the service to infer them.

SnapshotPreviews is described as the recommended iOS feeder for Sentry Snapshots, which is a narrower claim than being a general snapshot library, and it is worth holding that in mind when comparing it with other visual testing tools. The output format here is the PNG plus JSON sidecar pair that a visual diff service consumes, with the grouping key and the threshold carried in the metadata rather than in a filename convention.

The repository shows how the package is organised. There is a Package.swift, a Sources directory, a Tests directory, a PreviewsSupport directory and an XCFrameworkPackage.swift, along with a build.sh and a .spi.yml for the Swift Package Index. The Examples directory holds a demo app, a demo app built around XCFrameworks, a multi-module demo and a unit test migration example, which are the four situations where a package like this tends to break.

The package is MIT licensed, and the repository is not archived with the last push on 2026-10-08. The recent releases are v0.20.0 on 2026-10-08, v0.19.0 on 2026-09-15 and v0.18.0 on 2026-07-07, which is a steady monthly cadence for a package still moving between versions.

One limitation to state plainly. The images are previews, not full screen renders of a running app, so a preview that depends on runtime data, network state or a particular device configuration will not match what a user sees. The package solves the problem of keeping a visual record of the previews a team already reviews, not the problem of screenshotting the shipped application.

## Conclusion

Adopt SnapshotPreviews if your team already reviews UI in Xcode previews and wants those same views captured as images in CI, because it removes the duplicated preview fixtures that a separate snapshot test would need. Do not adopt it if you need pixel-exact baselines, since the exported images are meant for a visual diff service with a configurable threshold rather than for byte comparison. Verify first by running one test scheme with TEST_RUNNER_SNAPSHOTS_EXPORT_DIR set and checking the PNG and JSON files it writes, and confirm the naming of previews that use the #Preview macro before you rely on grouping.

## FAQ

### How do I generate snapshot images from Xcode previews with SnapshotPreviews?

Link the SnapshottingTests product to your XCTest target, add the package as a Swift Package Manager dependency, and create a test class inheriting from SnapshotTest with no test methods. The package discovers the previews at runtime and adds one test per preview, attaching each rendered PNG to the results bundle by default.

### How do I export snapshots to a directory for CI?

Run the test with TEST_RUNNER_SNAPSHOTS_EXPORT_DIR set to the target path, for example TEST_RUNNER_SNAPSHOTS_EXPORT_DIR="$PWD/snapshot-images" xcodebuild test with your scheme and destination. The package then writes the PNG and its JSON sidecar into that directory instead of attaching them to the .xcresult bundle, and sentry-cli build snapshots uploads the directory.

### What is the difference between the two SnapshotPreviews environment variables?

TEST_RUNNER_SNAPSHOTS_EXPORT_DIR renders the previews and writes the images and sidecars to that directory. TEST_RUNNER_SNAPSHOTS_ALL_IMAGE_NAMES_FILE writes only the discovered image names to a file and returns without rendering, which supports selective testing workflows. The two modes are mutually exclusive, and setting the names file means no images are rendered or exported.

## Sources

- [getsentry/SnapshotPreviews on GitHub](https://github.com/getsentry/SnapshotPreviews)
- [License: MIT](https://github.com/getsentry/SnapshotPreviews/blob/Update-button-shape/LICENSE)
- [Project website](https://docs.sentry.io/product/snapshots/)
- [README](https://github.com/getsentry/SnapshotPreviews/blob/Update-button-shape/README.md)
- [Releases](https://github.com/getsentry/SnapshotPreviews/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/getsentry-snapshotpreviews
