SnapshotPreviews

repository·main·Indexed 20 days ago

https://github.com/getsentry/snapshotpreviews

A library that allows developers to generate snapshot images directly from Xcode Previews (SwiftUI or UIKit) without manual test code. It supports exporting snapshots for visual diffing in Sentry Snapshots or other services, provides tools for migrating unit tests to PreviewProviders, and includes functionality to scope previews to specific modules in multi-module iOS projects.

Tokens
5.9K
Snippets
15
Records
22
Agent score
70%

What's inside SnapshotPreviews

  1. Use PreviewVariants for automated coverage

    main

    The PreviewVariants view simplifies snapshot testing by ensuring consistent variants (like dark mode, RTL, or accessibility) are rendered for every preview. SnapshotTest renders every variant emitted by the preview, creating a unique snapshot image and sidecar for each.

    struct MyView_Previews: PreviewProvider {
      static var previews: some View {
        PreviewVariants(layout: .sizeThatFits) {
          MyView(mode: .loaded)
            .previewVariant(named: "My View - Loaded")
    
          MyView(mode: .loading)
            .previewVariant(named: "My View - Loading")
    
          MyView(mode: .error)
            .previewVariant(named: "My View - Error")
        }
      }
    }
  2. Scope snapshot previews to specific modules

    main

    In multi-module iOS projects where a host app links several framework targets, a single SnapshotTest bundle may discover previews from every linked module by default.

    To prevent this and scope previews to specific modules, you can use the following overrides:

    • snapshotPreviewModules(): Define which modules should contribute previews.
    • excludedSnapshotPreviewModules(): Define which modules should be ignored during discovery.
  3. Follow snapshot best practices for determinism

    main

    Snapshot previews must be deterministic to ensure the same pixels are rendered in Xcode, local tests, and CI.

    Avoid:

    • Live network calls
    • Timers
    • Animations that do not settle
    • Locale-dependent data
    • Dates generated from the current clock

    Prefer:

    • Fixed fixtures
    • Mocked dependencies
  4. Set up a Preview Gallery for internal builds

    main

    The PreviewGallery is an interactive SwiftUI view that turns your previews into a browsable gallery. This is useful for internal distribution builds where Xcode is unavailable.

    Setup Steps

    1. Link the product: Link your app target to the PreviewGallery product.
    2. Configure Optimization: Disable whole-module optimization in your internal build configuration to ensure preview metadata is retained.
    3. Handle Compilation Conditions: If your previews are wrapped in #if DEBUG, add a custom flag like PREVIEW_GALLERY to your internal build configuration (SWIFT_ACTIVE_COMPILATION_CONDITIONS).
    4. Include Resources: Ensure any assets or JSON fixtures in Preview Content are included in the internal gallery build.

    Implementation Example

    import SwiftUI
    import PreviewGallery
    
    struct InternalSettingsView: View {
      var body: some View {
        NavigationStack {
          Form {
            Section("Previews") {
              NavigationLink("Open Gallery") { PreviewGallery() }
            }
          }
          .navigationTitle("Internal Settings")
        }
      }
    }
  5. Export snapshots for Sentry

    main

    To use SnapshotPreviews with Sentry Snapshots, follow these two steps:

    1. Export snapshots from your test run

    Set the TEST_RUNNER_SNAPSHOTS_EXPORT_DIR environment variable during your xcodebuild test invocation. This causes SnapshotTest to write PNG images and JSON metadata sidecars directly to that directory instead of the .xcresult bundle.

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

    2. Upload to Sentry

    Using sentry-cli

    Use sentry-cli version 3.4.0 or later. Point the command at your export directory:

    sentry-cli build snapshots "$PWD/snapshot-images" \
      --auth-token "$SENTRY_AUTH_TOKEN" \
      --app-id com.example.MyApp \
      --project my-ios-project

    Using Fastlane

    If your CI uses Fastlane, follow the official Sentry iOS Snapshots setup guide for Fastlane configuration.

  6. Integrate via Binary Frameworks (XCFrameworks)

    main

    If not using Swift Package Manager, you can use prebuilt XCFrameworks. Note that Xcode does not infer transitive dependencies for manually added frameworks, so you must link the full dependency set for your specific target.

    Dependency Mapping

    • App target (Preferences): SnapshotPreferences.xcframework, SnapshotSharedModels.xcframework.
    • App target (Gallery): PreviewGallery.xcframework, SnapshotPreviewsCore.xcframework, SnapshotPreferences.xcframework, SnapshotSharedModels.xcframework, PreviewsSupport.xcframework.
    • XCTest snapshot/layout target: SnapshottingTests.xcframework, SnapshotPreviewsCore.xcframework, SnapshotSharedModels.xcframework, PreviewsSupport.xcframework.
    • Accessibility/UI-test target: SnapshottingTests.xcframework, Snapshotting.xcframework, SnapshotPreviewsCore.xcframework, SnapshotSharedModels.xcframework, PreviewsSupport.xcframework.
  7. Migrate unit tests to PreviewProviders

    main

    To migrate existing tests from swift-snapshot-testing to SnapshotPreviews, you must move your test logic from a unit test target into your main application target and transform the test class into a PreviewProvider. This allows snapshot test function calls to be automatically converted into SwiftUI previews.

    Follow these steps:

    1. Add the helper file: Add SnapshotTest.swift to your app target. This file contains the logic that converts assertSnapshot calls into previews.
    2. Relocate the test file: Copy your existing test file from your unit test target into your main application target.
    3. Clean up imports: Remove import XCTest and import SnapshotTesting from the file.
    4. Update the class definition:
      • Change the base class from XCTestCase to SnapshotTest.
      • Add conformance to PreviewProvider.

    The SnapshotTest base class automatically intercepts calls to assertSnapshot and handles the conversion to a preview.

    import SwiftUI
    
    // Add the conformance to `PreviewProvider` when extending `SnapshotTest`
    // The `SnapshotTest` base class automatically handles turning calls to
    // assertSnapshot into previews.
    class ExampleSnapshotTest: SnapshotTest, PreviewProvider {
    
      func testContentViewSnapshot() {
        assertSnapshot(of: ContentView(), as: .image)
      }
    }
  8. Modify system SDKs to enable PreviewsSupport locally

    main

    To build PreviewsSupport locally, you must manually modify the .swiftinterface files within your Xcode SDKs to expose the necessary preview source symbols. This is required because these symbols are not present in the standard swiftinterface for SwiftUI and UIKit.

    For SwiftUI

    Append the following to Xcode.app/Contents/Developer/Platforms/[iPhoneSimulator/iPhoneOS/MacOSX].platform/Developer/SDKs/[iPhoneSimulator/iPhoneOS/MacOSX].sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/*.swiftinterface:

    @available(iOS 17.0, macOS 14.0, *)
    public struct ViewPreviewSource {
      public var makeView: @_Concurrency.MainActor () -> any SwiftUI.View
    }

    For UIKit

    Append the following to Xcode.app/Contents/Developer/Platforms/[iPhoneSimulator/iPhoneOS].platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework/Modules/UIKit.swiftmodule/*.swiftinterface:

    @available(iOS 17.0, macOS 14.0, *)
    public struct UIViewPreviewSource {
      public var makeView: @_Concurrency.MainActor () -> UIKit.UIView
    }
    
    @available(iOS 17.0, macOS 14.0, *)
    public struct UIViewControllerPreviewSource {
      public var makeViewController: @_Concurrency.MainActor () -> UIKit.UIViewController
    }

    For Mac Catalyst

    Modify the corresponding files in Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/iOSSupport/System/Library/Frameworks/.

  9. Provide unique display names for previews

    main

    To ensure that XCTest results and exported filenames/metadata are clear, give every preview a unique display name. Display names should be unique within each PreviewProvider or within a single file when using the #Preview macro.

    struct MyView_Previews: PreviewProvider {
      static var previews: some View {
        MyView().previewDisplayName("My Display Name")
      }
    }
    
    #Preview("My Display Name") {
      MyView()
    }
  10. Detect the snapshot environment in code

    main

    You can detect if code is running within a snapshot environment by checking the SNAPSHOTS_RUNNING_FOR_PREVIEWS environment variable. This allows you to disable preview-unfriendly behavior like logging, analytics, or network calls. Set SNAPSHOTS_RUNNING_FOR_PREVIEWS=1 in your unit test scheme to mirror Xcode's behavior.

    extension ProcessInfo {
      var isRunningPreviews: Bool {
        environment["SNAPSHOTS_RUNNING_FOR_PREVIEWS"] == "1"
      }
    }
  11. Generate snapshots from Xcode previews

    main

    To generate snapshots, create a test class that inherits from SnapshotTest. You do not need to write individual test functions; SnapshotPreviews discovers and adds a test for every preview at runtime.

    By default, rendered previews are attached to the XCTest results bundle as PNGs. To export them to a local directory instead, run xcodebuild test with the TEST_RUNNER_SNAPSHOTS_EXPORT_DIR environment variable set.

    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
      }
    }