Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 044 / 100

Snapshot testing SwiftUI: a hands-on guide for engineers

Catch visual regressions.

Expona Admin6 min read

Snapshot testing SwiftUI: a hands-on guide for engineers

Visual regressions slip through code review more often than we'd like to admit. A teammate tweaks a button's padding, another changes a color constant, and suddenly your app looks different across three views—but the unit tests pass. Snapshot testing SwiftUI gives us the precision to catch those changes before they reach production, and in this guide we'll walk through building a robust snapshot testing strategy using swift-snapshot-testing.

Why snapshot testing matters in SwiftUI

Unit tests verify logic; snapshot tests verify appearance. When you're working with SwiftUI, where your UI is composed of small, reusable views, a single line change can have ripple effects across your entire app. A snapshot test captures a reference image of your view and compares future renders against it. If the view changes—intentionally or accidentally—the test fails and shows you a diff.

This is especially valuable in e-commerce and SaaS products where visual consistency directly affects user trust and conversion rates. We've seen teams catch color regressions, text truncation issues, and layout shifts that would have gone unnoticed until QA or production.

Setting up swift-snapshot-testing

The swift-snapshot-testing library, maintained by Point-Free, is the industry standard for SwiftUI snapshot testing. It integrates cleanly with XCTest and generates deterministic snapshots across devices and configurations.

Installation via SPM:

swift
// In your Package.swift or Xcode's Add Package dialog
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.15.0")

Add the dependency to your test target, then import it:

swift
import SnapshotTesting
import SwiftUI
import XCTest

Create a basic test file to confirm everything works:

swift
class LoginViewSnapshotTests: XCTestCase {
    func testLoginViewLayout() {
        let view = LoginView()
        assertSnapshot(matching: view, as: .image)
    }
}

Run this test once with the record flag to generate your baseline. Expona projects typically organize snapshots in a __Snapshots__ folder alongside test files, making diffs easy to review in pull requests.

Recording reference images

The first time you run a snapshot test, it has no baseline to compare against. You need to record the reference image. This is intentional; you review and approve the baseline before the test becomes a regression detector.

Recording on the command line:

swift
// Run with record mode enabled
xcodebuild test -scheme YourScheme -configuration Debug \
  -derivedDataPath build \
  -testPlan YourTestPlan \
  -only-testing YourTarget/LoginViewSnapshotTests/testLoginViewLayout \
  -test-iterations 1 2>&1 | tee test.log

// Or in code, temporarily add this to your test
func testLoginViewLayout() {
    let view = LoginView()
    assertSnapshot(matching: view, as: .image, record: true)
}

The library generates a PNG in your __Snapshots__ folder. Commit this reference image to version control. From that point forward, every test run compares the rendered view against this baseline.

Real-world example: In a fintech app we built, the snapshot for a transaction detail view included specific date formatting, amount colors (red for debit, green for credit), and status badges. When a designer updated the badge styling, the test failed immediately and showed the exact diff—preventing that change from reaching staging without approval.

Pro tip: Keep your test views simple and focused. Test one component per snapshot rather than full screens. This makes diffs clearer and tests faster to run.

Testing dark mode and Dynamic Type

Your view must look correct across environments, not just light mode on a standard font size. The library supports environment overrides that let us capture snapshots in different configurations.

Dark mode snapshot:

swift
class ButtonSnapshotTests: XCTestCase {
    func testPrimaryButtonLightMode() {
        let view = PrimaryButton(label: "Continue", action: {})
        assertSnapshot(matching: view, as: .image)
    }
    
    func testPrimaryButtonDarkMode() {
        let view = PrimaryButton(label: "Continue", action: {})
            .environment(\.colorScheme, .dark)
        assertSnapshot(matching: view, as: .image)
    }
}

Dynamic Type (accessibility) snapshot:

swift
func testCardViewWithExtraLargeText() {
    let view = ProductCard(title: "Premium Widget", price: "$29.99")
        .environment(\.sizeCategory, .accessibilityExtraLarge)
    assertSnapshot(matching: view, as: .image)
}

func testCardViewWithSmallText() {
    let view = ProductCard(title: "Premium Widget", price: "$29.99")
        .environment(\.sizeCategory, .small)
    assertSnapshot(matching: view, as: .image)
}

We recommend testing at least three size categories: .small, default, and .accessibilityExtraLarge. This catches text truncation, layout shifts, and spacing issues early. In a ride-hailing app, we discovered that driver names in the pickup summary truncated unexpectedly at extra-large text sizes—a snapshot test would have prevented that accessibility regression.

Multiple device sizes:

swift
func testCheckoutFormOnDifferentDevices() {
    let view = CheckoutForm()
    
    for device in [ViewImageConfig.iPhone12, .iPhone12ProMax, .iPad] {
        assertSnapshot(matching: view, as: .image(size: device.size), 
                      named: device.name)
    }
}

By parameterizing device sizes, you catch responsive layout issues without duplicating test logic.

Reviewing diffs in pull requests

Snapshot tests are only valuable if your team actually reviews the diffs before merging. GitHub, GitLab, and other platforms can render image diffs inline, making visual regression changes obvious during code review.

Best practices for PR review:

  • Intentional changes only: When snapshot tests fail, the diff must be intentional. If a designer changed a button color, update the reference image deliberately. If it's accidental, fix the code.
  • Include snapshots in .gitignore exceptions: Your snapshot reference images should be committed. Add this to .gitignore:
swift
__Snapshots__/
!__Snapshots__/**/*.png
  • Run tests in CI/CD: Snapshot tests must run in your continuous integration pipeline with deterministic rendering. Use the same Xcode version and environment across all runs to avoid flaky diffs caused by rendering differences.
  • Use descriptive naming: Name your snapshot tests to reflect what they verify. Instead of testView(), use testCheckoutSummaryWithLongProductName() or testEmptyStateDarkMode().

In practice, we've seen snapshot testing catch issues that would otherwise require QA time or user reports. A CRM app caught a misaligned notification banner across multiple screens. A travel booking flow caught a date picker that rendered incorrectly when the selected date was more than 30 days in the future. These are the kinds of visual regressions that slip through traditional testing.

Common mistakes

Recording snapshots with stub data that won't match reality: Use representative data when recording. If you record a user list with one item, then the test passes with 100 items, you've missed layout issues. Use test data that exercises edge cases.

Including non-deterministic elements in snapshots: Timestamps, random IDs, or live data fetches make snapshots flaky. Mock data and time; keep snapshots deterministic. Wrap your views in a fixed date environment if they display dates:

swift
let fixedDate = Calendar.current.date(from: DateComponents(year: 2024, month: 3, day: 15))!
let view = OrderTimeline()
    .environment(\.calendar, Calendar(identifier: .gregorian))
    .onAppear { MockDateProvider.current = fixedDate }
assertSnapshot(matching: view, as: .image)

Over-snapshotting: Don't snapshot every view. Focus on the views that ship to users: cards, forms, buttons, modals, and screens. Internal helper views rarely need snapshots. Aim for 20-40 snapshot tests per major feature, not 200.

Ignoring performance: Snapshot rendering can slow CI. Run snapshot tests in a separate job or tier to avoid blocking faster unit tests. Keep your snapshot test suite under 5-10 minutes total.

Key takeaways

  • Snapshot testing SwiftUI catches visual regressions that unit tests miss, providing a safety net for designers and product teams.
  • Use swift-snapshot-testing to record reference images once, then compare every render. Commit snapshots to version control so diffs are visible in pull requests.
  • Test multiple configurations: dark mode, Dynamic Type sizes, and device dimensions. One snapshot per configuration ensures accessibility and responsive layout work correctly.
  • Keep snapshots deterministic by mocking data and time. Review diffs intentionally during code review; every snapshot change must be deliberate.
  • Start small. Add snapshot tests to your most visible components—buttons, cards, modals—before expanding to full screens.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles