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.
Updated about 1 month ago