This year Xcode previews got a big upgrade with the new execution engine in Xcode 16. Apple has been giving us many new preview related features
such as UIKit support in #Preview
macros, @Previewable
to manage state, and PreviewModifier
to reuse data.
These make adopting previews easier and increase the popularity of preview driven development.
However, previews can be prone to unexpected breaks. For example, say someone on your team adds a property to a view
that uses @EnvironmentObject
. If they forget to update every callsite to set the environment, the app will crash.
Any preview that uses the view without an environment will also crash when Xcode tries to render the preview.
This problem is even tricker because Xcode doesn’t have a built-in way
to test that previews will not crash without manually testing each one - this would take an impossibly long time. Some companies
like Doordash
and Handshake have written
about how they use previews for their snapshot tests - one technique to catch failing previews. In this
post, we’ll look at an even simpler way - writing a unit test that does a layout pass of all previews using
Emerge's open-source SnapshotPreviews Swift Package.
An example
Consider these two views in different files:
// Title.swift
struct Title: View {
let title: String
var body: some View {
Text(title)
.font(.largeTitle)
}
}
#Preview {
Title(title: "Title")
}
// TitleSubtitle.swift
struct TitleSubtitle: View {
let title: String
let subtitle: String
var body: some View {
HStack {
Title(title: title)
Text(subtitle)
}
}
}
#Preview {
TitleSubtitle(title: "Title", subtitle: "subtitle")
}
Now a change is made to add an environment object theme in Title
final class Theme: ObservableObject {
@Published var textColor: Color = .gray
}
struct Title: View {
@EnvironmentObject var theme: Theme
let title: String
var body: some View {
Text(title)
.font(.largeTitle)
.foregroundStyle(theme.textColor)
}
}
#Preview {
Title(title: "Title")
.environmentObject(Theme()) // crashes without this!
}
This is correct for the preview in Title.swift
, but causes a crash in TitleSubtitle.swift
! It would be easily fixed if you knew the previews
that eventually use a Title
, but in a large codebase, this could be in a file you’ve never even seen before.
Adding tests
Instead of manually testing each preview, you can write a unit test that automatically runs every preview in your app or framework. Just link the SnapshotPreviews package to your test target and copy the following code:
import SnapshottingTests
// This is an XCTestCase you can run in Xcode
final class MyPreviewTest: PreviewLayoutTest { }
Test functions will be automatically added for every preview in the app. You can easily view the results in Xcode or automate it
to run as part of CI using Fastlane or xcodebuild
.
A successful result will look like this:
and the previous example will result in a crash with a stacktrace pinpointing the problem:
How it works
This works by reading the Swift type metadata in the app binary to find all protocol conformance records to PreviewProvider
. The conforming
type is dynamically initialized and called to get the SwiftUI View for each provider. Then, the view is decomposed into each preview using VariadicView.
A new test function is added to the test class for each discovered preview thanks, to the dynamic nature of the Objective-C runtime. Since this is automatic, you don’t have to write any code, just add the new previews to your app and they are tested!
Concluding thoughts
Writing tests always seems like the worst part of testing, and this is an easy way to get a lot of test coverage without having to write any tests! Typically, unit tests don’t include view related code, so this is also a great way to increase the code coverage percentage beyond the usual unit tests. Lastly, although the example we looked at in this post is not a bug in the production app, it still would waste developer time if it was not caught with a test because anyone using the affected previews would have to wait for a fix. Catching these issues pre-merge is a great way to speed up preview driven development!