iOS/macOS Snapshots

Supported types

📘

Snapshots are generated from Xcode Previews

Emerge Snapshot Testing works with anything represented by a PreviewProvider or #Preview macro.

Once you've setup your preview within Xcode, Emerge will alert you whenever there is a regression.

You can snapshot SwiftUI's View, UIKit's UIView and UIViewController. See Apple's documentation for more information on how to get started using previews.

SwiftUI usage

#Preview {
  ContentView()
}

struct MyPreviewProvider: PreviewProvider {
  var previews: some view {
    ContentView()
  }
}

UIKit usage

// UIViewController example
#Preview {
  let viewController = WeatherViewController()
  viewController.title = "Current Weather"
  return viewController
}

// UIView example
#Preview {
  let view = WeatherView()
  if let image = UIImage(systemName: "sun.max.fill") {
    view.icon = image
  }
  return view
}

Upload requirements

Emerge's snapshot testing works by automatically parsing all of the Xcode previews present in your app binary, and then instantiating them on a real simulator to generate images. This means the build must contain all the relevant code and resources needed to render each preview.

🚧

Common Issue

The most common cause of seeing few or no previews is from submitting release builds, so double check that the build configuration is set to debug when archiving/cutting your build.

If expected previews aren't showing up, we recommend:

  1. Ensure you are submitting a Debug configuration build. By default this disables features like Dead code stripping which can remove previews the linker thinks are unused.
  2. Confirm no linker settings like Dead code stripping or Whole Module Optimization are turned on manually for your build configuration.
  3. Check for any conditional preprocessor directives that might be preventing previews from compiling, e.g. #if SOME_FLAG.
  4. If your app is composed of dynamic frameworks, check if any have differing build settings.

Other requirements

The app must support arm64 devices and be able to run on a simulator (for iOS apps).

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.

Also please ensure that your minimum deployment is no higher than 17.5!

📘

Crash reporters

Crash reporters should be disabled at runtime in builds uploaded for snapshot tests. For more details see Crash Reporters (iOS).

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 on iOS 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.

Helper View

We recommend creating a custom View to help generate variants for all of your previews. For example, this snippet will generate light/dark mode snapshots for a phone and tablet device:

import Foundation
import SwiftUI

struct PreviewVariants<Content: View>: View {
  let content: Content

  init(@ViewBuilder _ content: () -> Content) {
    self.content = content()
  }

  var body: some View {
    Group {
      self.content
        .environment(\.colorScheme, .light)
        .preferredColorScheme(.light)
        .navigationBarHidden(true)
        .previewDevice("iPhone 11 Pro Max")
        .previewDisplayName("iPhone 11 Pro Max, light mode")

      self.content
        .environment(\.colorScheme, .dark)
        .preferredColorScheme(.dark)
        .navigationBarHidden(true)
        .previewDevice("iPhone 11 Pro Max")
        .previewDisplayName("iPhone 11 Pro Max, dark mode")

      self.content
        .environment(\.colorScheme, .light)
        .preferredColorScheme(.light)
        .navigationBarHidden(true)
        .previewDevice("iPad Air (5th generation)")
        .previewDisplayName("iPad Air, light mode")

      self.content
        .environment(\.colorScheme, .dark)
        .preferredColorScheme(.dark)
        .navigationBarHidden(true)
        .previewDevice("iPad Air (5th generation)")
        .previewDisplayName("iPad Air, dark mode")
    }
  }
}

You can customize the variants to your liking, say if you want to also take landscape and dynamic type snapshots too.

In your preview you would then call it like so:

struct SomeView_Preview: PreviewProvider {
  static var previews: some View {
    PreviewVariants {
      SomeView()
    }
  }
}

Note: this must be used in a PreviewProvider because the #Preview macro does not support expanding Group into separate previews.

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.

iOS 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. This ".profdata" file can be downloaded via:

http://api.emergetools.com/snapshots?uploadId=[your_upload_id]

Enabling coverage requires settings -profile-generate -profile-coverage-mapping in OTHER_SWIFT_FLAGS and -fprofile-instr-generate in OTHER_LDFLAGS. For an example, see this PR . To check if an app has code coverage information, look for the LLVM_COV segment in the binary.

The Composable Architecture (TCA)

By default we set the environment variable SWIFT_DEPENDENCIES_CONTEXTto be "preview"when launching the app.