How to implement snapshot tests in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: SnapshotTesting Updated: May 12, 2026
TL;DR

Add swift-snapshot-testing from Point-Free to your test target, then call assertSnapshot(of:as:) with an .image(layout:) strategy to capture and compare pixel-perfect PNG renders of any SwiftUI view. Run once with record: true to write baselines; every subsequent run diffs against them and fails on any change.

import XCTest
import SnapshotTesting
import SwiftUI

final class ProfileCardSnapshotTests: XCTestCase {
    func testProfileCard_light() {
        let sut = ProfileCard(name: "Ada Lovelace", role: "Engineer")
            .environment(\.colorScheme, .light)
        assertSnapshot(
            of: sut,
            as: .image(layout: .fixed(width: 393, height: 100))
        )
    }
}

Full implementation

The library wraps your View in a UIHostingController, renders it off-screen, and persists the result as a PNG in a __Snapshots__ folder next to your test file. On subsequent runs it diffs the live render against the stored PNG and fails the test on any pixel discrepancy, making regressions immediately visible in code review.

The example below covers the four most valuable snapshot scenarios for a production SwiftUI card component: light mode, dark mode, large Dynamic Type, and intrinsic sizing — each producing its own named PNG.

// ── Package.swift additions ────────────────────────────────────────
// dependencies:
//   .package(url: "https://github.com/pointfreeco/swift-snapshot-testing",
//            from: "1.17.0")
// targets (test target only):
//   .product(name: "SnapshotTesting", package: "swift-snapshot-testing")

import XCTest
import SnapshotTesting
import SwiftUI

// ── View under test ────────────────────────────────────────────────
struct ProfileCard: View {
    let name: String
    let role: String

    var body: some View {
        HStack(spacing: 16) {
            Image(systemName: "person.crop.circle.fill")
                .resizable()
                .frame(width: 56, height: 56)
                .foregroundStyle(.accent)
                .accessibilityLabel("Profile photo")
            VStack(alignment: .leading, spacing: 4) {
                Text(name)
                    .font(.headline)
                Text(role)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
            Spacer()
        }
        .padding()
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
        .padding(.horizontal)
    }
}

// ── Snapshot test suite ────────────────────────────────────────────
final class ProfileCardSnapshotTests: XCTestCase {

    /// Flip to `true` to write/overwrite reference PNGs, then flip back.
    private let isRecording = false

    // 1. Light mode — iPhone 16 Pro width
    func testProfileCard_light() {
        let sut = ProfileCard(name: "Ada Lovelace", role: "Principal Engineer")
            .environment(\.colorScheme, .light)
        assertSnapshot(
            of: sut,
            as: .image(layout: .fixed(width: 393, height: 104)),
            record: isRecording
        )
    }

    // 2. Dark mode — separate reference PNG
    func testProfileCard_dark() {
        let sut = ProfileCard(name: "Ada Lovelace", role: "Principal Engineer")
            .environment(\.colorScheme, .dark)
        assertSnapshot(
            of: sut,
            as: .image(layout: .fixed(width: 393, height: 104)),
            record: isRecording
        )
    }

    // 3. Accessibility Extra Large Dynamic Type
    func testProfileCard_accessibilityXL() {
        let sut = ProfileCard(name: "Ada Lovelace", role: "Principal Engineer")
            .environment(\.sizeCategory, .accessibilityExtraLarge)
        assertSnapshot(
            of: sut,
            as: .image(layout: .fixed(width: 393, height: 180)),
            record: isRecording
        )
    }

    // 4. sizeThatFits — let SwiftUI measure its own intrinsic size
    func testProfileCard_intrinsicSize() {
        let sut = ProfileCard(name: "Jo", role: "Dev")
        assertSnapshot(
            of: sut,
            as: .image(layout: .sizeThatFits),
            record: isRecording
        )
    }
}

// ── SwiftUI Preview ────────────────────────────────────────────────
#Preview("Light") {
    ProfileCard(name: "Ada Lovelace", role: "Principal Engineer")
        .padding(.vertical)
}

#Preview("Dark") {
    ProfileCard(name: "Ada Lovelace", role: "Principal Engineer")
        .padding(.vertical)
        .preferredColorScheme(.dark)
}

How it works

  1. Test-target-only dependency: Swift Package Manager links SnapshotTesting only to the test target, so it never enters your app binary or App Store submission. The __Snapshots__ PNGs live on disk alongside the .swift test file and are committed to git.
  2. assertSnapshot(of:as:record:): The of parameter accepts any View directly — the library wraps it in a UIHostingController internally. The as parameter selects the snapshotting strategy; .image produces a PNG diff.
  3. Layout strategies: .fixed(width:height:) renders at an explicit pixel size — ideal for cards and list rows. .sizeThatFits lets SwiftUI compute its own preferred size, which is better for intrinsically-bounded components like buttons or badges.
  4. Environment injection: Passing .environment(\.colorScheme, .dark) before the assertion renders dark mode without changing the simulator's global appearance. This lets a single test run cover both colour schemes — two separate named PNGs — without switching schemes in between.
  5. Record mode & CI: When record: true, the assertion always passes and writes a fresh PNG. Set it back to false before committing. In CI, pin the xcodebuild -destination to a fixed simulator (e.g., iPhone 16 Pro, iOS 17.5) so pixel output is deterministic across machines.

Variants

Full-screen view with NavigationStack

Use .device(config:) to render at an exact device resolution, including the navigation bar and safe-area insets. The ViewImageConfig catalogue ships with configs for common iPhone and iPad models.

func testSettingsScreen_iPhone16Pro() {
    let sut = NavigationStack {
        SettingsView()
    }
    .environment(\.colorScheme, .light)

    assertSnapshot(
        of: sut,
        as: .image(layout: .device(config: .iPhone13Pro)),
        record: isRecording
    )
}

Parameterised multi-device loop

Iterate over a device list to produce one named PNG per size class. Each file is independently diffed, so a regression on iPad Pro won't mask a clean iPhone run.

func testProfileCard_multiDevice() {
    let devices: [(String, ViewImageConfig)] = [
        ("iPhoneSE3",   .iPhoneSe),
        ("iPhone16Pro", .iPhone13Pro),
        ("iPadPro11",   .iPadPro11)
    ]
    for (name, config) in devices {
        assertSnapshot(
            of: ProfileCard(name: "Ada Lovelace", role: "Engineer"),
            as: .image(layout: .device(config: config)),
            named: name,
            record: isRecording
        )
    }
}

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement snapshot tests in SwiftUI for iOS 17+.
Use SnapshotTesting (pointfreeco/swift-snapshot-testing ≥1.17.0).
Cover light mode, dark mode, and accessibilityExtraLarge Dynamic Type.
Inject fixed data so all snapshots are fully deterministic.
Make all interactive elements accessible (VoiceOver labels).
Add a #Preview with realistic sample data.

Drop this prompt into Soarias during the Build phase right after scaffolding your feature screens — Claude Code will wire up the SPM dependency, generate the full test suite, and write the initial reference PNGs in a single pass so visual regression coverage is ready before your first TestFlight build.

Related

FAQ

Does this work on iOS 16?

The SnapshotTesting library itself supports iOS 13+, so asserting against an iOS 16 deployment target is fine. However, this guide uses the #Preview macro (iOS 17+) and newer SwiftUI material modifiers. If you target iOS 16, replace #Preview with PreviewProvider and swap any iOS 17-only APIs with their iOS 16 equivalents — the snapshot assertions themselves are unchanged.

How do I update a snapshot after an intentional UI change?

Temporarily set isRecording = true in your test class, run only the affected tests, and the library overwrites the old PNGs with fresh baselines. Flip isRecording back to false before committing. Alternatively, delete the specific PNG files from Finder — a missing reference triggers record mode automatically for that test only, leaving all other baselines intact.

What's the UIKit equivalent?

In UIKit you pass a UIViewController or UIView directly: assertSnapshot(of: myViewController, as: .image(on: .iPhone13Pro)). The API is identical — SnapshotTesting ships built-in strategies for both UIKit and SwiftUI types, so you can snapshot UIKit and SwiftUI views within the same test suite and even the same test file.

Last reviewed: 2026-05-12 by the Soarias team.