iOS Snapshots

Uploading a build

Snapshots automatically parses all Xcode previews in your app binary and uses them to generate images. This process also tests your previews to verify they render without errors. Ensure your build includes previews, often they are removed by dead code stripping or #if DEBUG checks. The default Xcode settings for a debug build will include previews, but you maybe need to select a different build configuration for your Xcode settings.

🚧

Dead code stripping

The default archive settings in Xcode builds with a "Release" configuration which strips out unused structs including previews from the app binary. A build without this setting enabled is needed to generate snapshots. Consider changing the build configuration from "Release" to "Debug" before making an archive if you use the default Xcode settings.

Other requirements

The app must support arm64 devices.

The app is re-signed before running for preview extraction, so it needs to be able to run without entitlements. For example, app groups and associated domains entitlements are removed.

Preview types

Snapshots support SwiftUI View, UIKit UIView and UIViewController. To make your previews visible to Emerge they need to be accessible from a PreviewProvider or #Preview macro.

Preventing flakiness

To reliably detect differences in images and prevent flaky diffs, each preview needs to render reproducibly. You can check if the environment variable EMERGE_IS_RUNNING_FOR_SNAPSHOTS is set to 1 to enable overrides such as mocking potentially volatile data. Commonly this applies to build versions, random numbers, or network responses. To help control for flakiness, HTTP requests through NSURL are blocked while generating snapshots.

Custom Precision

You can reduce the amount of precision required for images to be considered unchanged using EmergePrecisionModifier. Add it to your preview by linking SnapshotPreviewsCore from the SDK and calling .emergeSnapshotPrecision(...) For example the following preview will only be marked as changed if the diff is > 1%.

struct MyComponent_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
            .previewDisplayName("Light")
            .emergeSnapshotPrecision(0.99)
    }
}

The percentage difference is calculated based on the perceptual difference between pixels, with colors that are more different having a higher percent difference.

Components and variants

Each PreviewProvider type is a single component, but can provide multiple variants. For example if your code looks like this:

struct MyComponent_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
            .previewDisplayName("Light")
        MyView()
            .preferredColorScheme(.dark)
            .previewDisplayName("Dark")
    }
}

Then MyComponent will have two variants, named Light and Dark. When a component has multiple variants, we recommend specifying a name for each case so they can be easily distinguished when viewing the generated snapshots.

Device variants

Snapshot Testing has support for 3 devices:

  • iPhone 11 Pro Max
  • iPhone 8
  • iPad Air (5th generation)

You can define the preview device with .previewDevice().

struct MyComponent_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
            .previewDevice("iPhone 8")
        MyView()
            .previewDevice("iPad Air (5th generation)")
    }
}

By default the selected device is the iPhone 11 Pro Max, and if you have a different one, they will be mapped using the following rules:

  • Any iPad (mini, Pro, Air) maps to iPad Air (5th generation).
  • All iPhones with home button map to iPhone 8.
  • Everything else will use the iPhone 11 Pro Max.

Accessibility variants

You can snapshot the accessibility elements of your apps by calling the function emergeAccessibility(true)

struct MyComponent_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
        MyView()
            .emergeAccessibility(true)
    }
}

This will use AccessibilitySnapshot to visualize the accessibility elements on your view.

Layout

Emerge respects the preview layout in previewLayout() when generating snapshots. The default .device layout will result in a full screen preview. We recommend using a .fixed or .sizeThatFits layout to customize the dimensions of your snapshot.

Scroll views

Scroll views are automatically expanded to a height that fits their contentSize. This only happens for the first scroll view discovered in a preview, and only for the height (not width). Layouts with a fixed size (PreviewLayout.fixed) will not be expanded.

Interface orientation

Emerge can generate snapshots in both portrait and landscape orientations, respecting the selected orientation with previewInterfaceOrientation(). Keep in mind that interface orientation is different from device orientation, if your code is using the orientation, you must rely on the interface orientation provided by UIWindowsScene instead of UIDevice. A simple way to get it is with this code:

UIApplication.shared.windows
  .first?
  .windowScene?
  .interfaceOrientation

Code Coverage

Snapshot tests automatically generate code coverage reports as a ".profdata" file if your uploaded binary is built with code coverage information included. To test for code coverage information in the binary, look for the LLVM_COV segment in your binary.

Generate snapshots locally

Emerge also offers a Swift package generate snapshots locally for debugging.

  • Install the Swift Package in your project
  • Add a UI test target with SnapshottingTests as a dependency.
  • Create your test that inherits from SnapshottingTests.PreviewTest

See the repo for full documentation.

Excluding snapshots

If you want to exclude some snapshots from being captured, we support including an emerge_config.yaml file inside the top-level of your app upload. For example, your app may link against a dynamic framework that has its own previews you don't wish to see. We support excluding by the exact preview name and also by a regex. The schema for this YAML file is as follows:

version: 1.0
snapshots:
  ios:
    osVersions:
      - 17.2
    excludedPreviews:
      - type: exact
        value: TestModule.TestPreviewProviderType1
      - type: exact
        value: TestModule.TestPreviewProviderType2
      - type: regex
        value: ^TestModule.*$
    envVariables:
      var1: 1
      var2: 2
      var3: 3

If you want to exclude a preview but still have the view's body be evaluated, you can use a custom extension on View like this:

extension View {
  @ViewBuilder func emergeDisabled() -> some View {
      if ProcessInfo.processInfo.environment["EMERGE_IS_RUNNING_FOR_SNAPSHOTS"] == "1" {
        EmptyView()
      } else {
        self
      }
  }
}

Note that your the preview provider's body property will still need to be evaluated without any exceptions for this to work. If the preview crashes when running it will need to be excluded with the yaml file instead.