Snapshot testing is usually introduced with screenshots: render a view, save an image, and fail the test when the pixels change.
That is useful, but it undersells the design of swift-snapshot-testing. In the library’s core, a snapshot is not necessarily an image and the input is not necessarily a view. Snapshot testing is modeled as a transformation from one value into another representation, followed by a comparison of that representation with a stored reference.
That small separation is what makes the package interesting to read.
Two jobs instead of one screenshot API
The central type is:
public struct Snapshotting<Value, Format> {
public var pathExtension: String?
public var diffing: Diffing<Format>
public var snapshot: (Value) -> Async<Format>
}
There are two generic parameters because the thing under test and the thing stored for comparison do not have to be the same type.
A SwiftUI view can become a UIImage. An Encodable value can become a JSON string. A request can become a textual representation. Your own domain value can become whatever stable representation is useful for a regression test.
The second half is Diffing<Format>. It knows how to turn a snapshot format into Data, reconstruct it from Data, and compare two values. A mismatch can return both a message and attachments describing the difference.
So the responsibilities are deliberately separate: Snapshotting produces a representation; Diffing stores and compares it. This is more flexible than putting capture, persistence and comparison rules into a single UI-specific assertion.
Example: a SwiftUI state as an image
Suppose a small status badge must remain readable in dark mode at a fixed size:
import SnapshotTesting
import SwiftUI
import UIKit
struct StatusBadge: View {
let title: String
var body: some View {
Text(title)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(.blue)
.foregroundStyle(.white)
.clipShape(Capsule())
}
}
@MainActor
func testStatusBadgeDarkMode() {
let badge = StatusBadge(title: "Connected")
assertSnapshot(
of: badge,
as: .image(
layout: .fixed(width: 180, height: 60),
traits: UITraitCollection(userInterfaceStyle: .dark)
)
)
}
This uses the current SwiftUI image strategy. Internally, that strategy creates a UIHostingController, determines the requested layout, and delegates rendering to the lower-level view snapshot machinery.
The source also shows an important detail: the SwiftUI image strategy itself is composed from a lower-level image strategy using asyncPullback. That pattern appears throughout the package.
The operation that makes the design reusable
Snapshotting exposes pullback:
public func pullback<NewValue>(
_ transform: @escaping (NewValue) -> Value
) -> Snapshotting<NewValue, Format>
If a strategy already knows how to snapshot Value, pullback lets it snapshot NewValue when you can transform NewValue into Value.
The built-in strategies make this concrete. The JSON strategy for Encodable values pretty-prints and sorts JSON keys, converts the result to a string, and reuses the line-based string strategy. The .description strategy similarly reuses line snapshots after transforming a value with String.init(describing:).
You can use the same idea in application tests.
Example: snapshot the contract you care about
Imagine a receipt has many implementation details, but a test only cares about the stable summary presented to another part of the system:
import SnapshotTesting
struct Receipt {
let number: Int
let customer: String
let totalCents: Int
}
let receiptSummary =
Snapshotting<String, String>.lines.pullback { (receipt: Receipt) in
"""
Receipt #\(receipt.number)
Customer: \(receipt.customer)
Total cents: \(receipt.totalCents)
"""
}
func testReceiptSummary() {
let receipt = Receipt(
number: 42,
customer: "Blob",
totalCents: 12_500
)
assertSnapshot(of: receipt, as: receiptSummary)
}
There is no custom assertion here. The application decides how a Receipt becomes stable text, while the library reuses its existing text persistence and line diff.
This is one of the strongest ideas in the package: a snapshot strategy can describe the representation that matters to a test instead of forcing the test to snapshot the entire object.
What happens when an assertion runs
At the public API, assertSnapshot(of:as:...) delegates the work to verifySnapshot. The strategy produces the new snapshot, the library locates the reference file, and the strategy’s Diffing value compares the stored and new formats.
Current source defines four record modes: .all, .missing, .failed, and .never. The last mode does not record missing snapshots and is explicitly documented as appropriate for CI where retries should not unexpectedly pass after generating references.
Configuration can be scoped with withSnapshotTesting. Internally, SnapshotTestingConfiguration.current is a @TaskLocal, so synchronous and asynchronous operations can inherit scoped configuration without relying only on a single global switch.
The package integrates with Swift Testing as well as XCTest. Release 1.19.0 added Swift Testing attachment support, and 1.19.1 followed with fixes for recording and image attachments.
Image snapshots are only one strategy family
The repository contains strategies for much more than UIKit screenshots: strings, encodable values, URL requests, layers, web views and several Apple UI types.
The architecture makes those strategies easier to understand because many are transformations of simpler strategies. Line snapshots own text serialization and line diffing, so JSON can focus on producing deterministic JSON text. A view-controller image strategy can focus on preparing and rendering the controller, then reuse image comparison.
If you can transform a new application type into a format the library already knows how to diff, you often do not need to implement the full snapshot pipeline.
Where snapshot tests become fragile
The abstraction cannot remove instability from the representation you choose.
The project’s README warns that image snapshots should be compared using the same simulator that recorded the reference to avoid discrepancies. UI rendering depends on platform behavior, fonts, simulator/runtime versions and layout details. A pixel reference is therefore a much more environment-sensitive contract than a sorted JSON string.
The source exposes controls such as precision, perceptualPrecision, layouts and trait collections, but those define the comparison. They do not make every screenshot portable across environments.
There is also current evidence that UI rendering remains an active compatibility surface. Open issue #1089 reports a crash in the view setup path on an iOS 26.2 simulator with version 1.19.2. That is a reported issue, not evidence that every current setup is affected, but it is a useful reminder that snapshot infrastructure sits on top of changing Apple UI frameworks.
For many domain-level tests, a text or data representation can be a better contract than an image. For visual regressions, an image is exactly the contract you need.
A package that has kept adapting
The current package manifest uses Swift tools 6.0 and Swift language mode 5. It declares iOS 13, macOS 10.15, tvOS 13 and watchOS 6 as minimum Apple platforms. The package exposes three products: SnapshotTesting, InlineSnapshotTesting, and SnapshotTestingCustomDump.
The package-level dependencies are swift-custom-dump and swift-syntax; notably, the core SnapshotTesting target declares no target dependencies on either. They are used by the additional products.
The latest verified release is 1.19.4 from July 28, 2026. The 1.19 series has included Swift Testing attachments, image-diff improvements, SwiftSyntax/compiler compatibility work, a UIHostingController safe-area fix, and smaller follow-up fixes. There is also an open enhancement issue discussing deeper library-level Swift concurrency support.
That activity matters because snapshot testing touches compilers, testing frameworks and UI rendering, all of which move underneath the library.
What is worth taking from the source
The most reusable lesson in swift-snapshot-testing is not how to save a screenshot. It is the decision to model snapshot testing as transform + diff.
Snapshotting<Value, Format> answers: what stable representation should this value become?
Diffing<Format> answers: how is that representation stored and compared?
And pullback connects existing strategies to new types without requiring those types to know anything about snapshot testing.
That makes the package useful for UI regression tests, but it also makes its source a compact example of how small generic abstractions can turn one testing technique into an extensible system.
