Making SwiftUI Previews Work For You
Rate this article
Canvas previews are an in-your-face feature of SwiftUI. When you create a new view, half of the boilerplate code is for the preview. A third of your Xcode real estate is taken up by the preview.
Despite the prominence of the feature, many developers simply delete the preview code from their views and rely on the simulator.
In past releases of Xcode (including the Xcode 13 betas), a reluctance to use previews was understandable. They'd fail for no apparent reason, and the error messages were beyond cryptic.
I've stuck with previews from the start, but at times, they've felt like more effort than they're worth. But, with Xcode 13, I think we should all be using them for all views. In particular, I've noticed:
- They're more reliable.
- The error messages finally make sense.
- Landscape mode is supported.

I consider previews a little like UI unit tests for your views. Like with unit tests, there's some extra upfront effort required, but you get a big payback in terms of productivity and quality.
In this article, I'm going to cover:
- What you can check in your previews (think light/dark mode, different devices, landscape mode, etc.) and how to do it.
- Reducing the amount of boilerplate code you need in your previews.
- Writing previews for stateful apps. (I'll be using Realm, but the same approach can be used with Core Data.)
- Troubleshooting your previews.
One feature I won't cover is using previews as a graphical way to edit views. One of the big draws of SwiftUI is writing everything in code rather than needing storyboards and XML files. Using a drag-and-drop view builder for SwiftUI doesn't appeal to me.
95% of the examples I use in this article are based on a BlackJack training app. You can find the final version in the repo.
Build better mobile apps with Atlas Device Sync: Atlas Device Sync is a fully-managed mobile backend-as-a-service. Leverage out-of-the-box infrastructure, data synchronization capabilities, built-in network handling, and much more to quickly launch enterprise-grade mobile apps. Get started now by build: Deploy Sample for Free!
- Xcode 13+
- iOS 15+
- Realm-Cocoa 10.17.0+
Note:
- I've used Xcode 13 and iOS 15, but most of the examples in this post will work with older versions.
- Previewing in landscape mode is new in Xcode 13.
- The
buttonStyle
modifier is only available in iOS 15. - I used Realm-Cocoa 10.17.0, but earlier 10.X versions are likely to work.
Previews let you see what your view looks like without running it in a simulator or physical device. When you edit the code for your view, its preview updates in real time.
This section shows what aspects you can preview, and how it's done.
When you create a new Xcode project or SwiftUI view, Xcode adds the code for the preview automatically. All you need to do is press the "Resume" button (or CMD-Alt-P).

The preview code always has the same structure, with the
View
that needs previewing (in this case, ContentView
) within the previews
View
:Most of your views will require that the enclosing view pass in parameters. Your preview must do the same—you'll get a build error if you forget.
The preview code needs to pass in those values, just like any embedding view:
In a chat app, I have a
LoginView
that updates the username
binding that's past from the enclosing view:The simplest way to create a binding in your preview is to use the
constant
function:In your view hierarchy, you only add a
NavigationView
at a single level. That NavigationView
then wraps all subviews.When previewing those subviews, you may or may not care about the
NavigationView
functionality. For example, you'll only see titles and buttons in the top nav bar if your preview wraps the view in a NavigationView
.
To preview the title, my preview code needs to wrap
PracticeView
in a NavigationView
:
Sometimes, you don't need to preview your view in the context of a full device screen. My
CardView
displays a single playing card. Previewing it in a full device screen just wastes desk space:
We can add the
previewLayout
modifier to indicate that we only want to preview an area large enough for the view. It often makes sense to add some padding
as well:
It can be quite a shock when you finally get around to testing your app in dark mode. If you've not thought about light/dark mode when implementing each of your views, then the result can be ugly, or even unusable.
Previews to the rescue!
Returning to
CardView
, I can preview a card in dark mode using the preferredColorScheme
view modifier:
That seems fine, but what if I previewed a spade instead?

That could be a problem.
Adding a white background to the view fixes it:

Sometimes, previewing a single instance of your view doesn't paint the full picture. Just look at the surprise I got when enabling dark mode for my card view. Wouldn't it be better to simultaneously preview both hearts and spades in both dark and light modes?
You can create multiple previews for the same view using the
Group
view:
A preview of a single view in isolation might look fine, but what will they look like within a broader context?

But, the app will never display a single
DecisionCell
. They'll always be in a grid. Also, the text, background color, and border vary according to state. To create a more realistic preview, I created some sample data within the view and then composed multiple DecisionCell
s using vertical and horizontal stacks:I could then see that the black border didn't work too well in dark mode:

Switching the border color from
black
to primary
quickly fixed the issue:
Previews default to portrait mode. Use the
previewInterfaceOrientation
modifier to preview in landscape mode instead:
Previews default to the simulator device that you've selected in Xcode. Chances are that you want your app to work well on multiple devices. Typically, I find that there's extra work needed to make an app I designed for the iPhone work well on an iPad.
The
previewDevice
modifier lets us specify the device type to use in the preview:
You can find the names of the available devices from Xcode's simulator menu, or from the terminal using
xcrun simctl list devices
.In the bottom-left corner of the preview area, there's a pin button. Pressing this "pins" the current preview so that it's still shown when you browse to the code for other views:

This is useful to observe how a parent view changes as you edit the code for the child view:

At the start of this article, I made a comparison between previews and unit testing. Live previews mean that you really can test your views in isolation (to be accurate, the view you're testing plus all of the views it embeds or links to).
Press the play button above the preview to enter live mode:

You can now interact with your view:

As you may have noticed, some of my previews now have more code than the actual views. This isn't necessarily a problem, but there's a lot of repeated boilerplate code used by multiple views. Not only that, but you'll be embedding the same boilerplate code into previews in other projects.
To streamline my preview code, I've created several view builders. They all follow the same pattern—receive a
View
and return a new View
that's built from that View
.I start the name of each view builder with
_Preview
to make it easy to take advantage of Xcode's code completion feature.To use this view builder in a preview, simply pass in the
View
you're previewing:
To use this view builder in a preview, simply pass in the
View
you're previewing:
_PreviewNoDevice
returns a view built from adding the previewLayout
modifier and adding `padding to the input view:To use this view builder in a preview, simply pass in the
View
you're previewing:
_PreviewDevices
returns a Group
containing a copy of the View
for each device type. You can modify devices
in the code to include the devices you want to see previews for:I'd be cautious about adding too many devices as it will make any previews using this view builder slow down and consume resources.
To use this view builder in a preview, simply pass in the
View
you're previewing:
Each view builder receives a view and returns a new view. That means that you can compose the functions by passing the results of one view builder to another. In the extreme case, you can use up to three on the same view preview:
This produces 12 views to cover all permutations of orientation, appearance, and device.
For each view, you should consider which modifiers add value. For the
CardView
, it makes sense to use _PreviewNoDevice
and _PreviewColorSchem
e, but previewing on different devices and orientations wouldn't add any value.Often, a SwiftUI view will fetch state from a database such as Realm or Core Data. For that to work, there needs to be data in that database.
Previews are effectively running on embedded iOS simulators. That helps explain how they are both slower and more powerful than you might expect from a "preview" feature. That also means that each preview also contains a Realm database (assuming that you're using the Realm-Cocoa SDK). The preview can store data in that database, and the view can access that data.
In the BlackJack training app, the action to take for each player/dealer hand combination is stored in Realm. For example,
DefaultDecisionView
uses @ObservedResults
to access data from Realm:To ensure that there's data for the previewed view to find, the preview checks whether the Realm database already contains data (
Decisions.areDecisionsPopulated
). If not, then it adds the required data (Decisions.bootstrapDecisions()
):DefaultDecisionView
is embedded in DecisionMatrixView
and so the preview for DecisionMatrixView
must also conditionally populate the Realm data. In turn, DecisionMatrixView
is embedded in PracticeView
, and PracticeView
in ContentView
—and so, they too need to bootstrap the Realm data so that it's available further down the view hierarchy.This is the implementation of the bootstrap functions:
The BlackJack training app uses a standalone Realm database. But what happens if the app is using Realm Sync?
One option could be to have the SwiftUI preview sync data with your backend Realm service. I think that's a bit too complex, and it breaks my paradigm of treating previews like unit tests for views.
I've found that the simplest solution is to make the view aware of whether it's been created by a preview or by a running app. I'll explain how that works.
Its preview code bootstraps the embedded realm:
The app adds bootstrap as an extension to Realm:
A complication is that
AuthorView
is embedded in ChatBubbleView
. For the app to work, ChatBubbleView
must pass the synced realm configuration to AuthorView
:But, when previewing
ChatBubbleView
, we want AuthorView
to use the preview's local, embedded realm (not to be dependent on a Realm back-end app). That means that ChatBubbleView
must check whether or not it's running as part of a preview:The preview is then responsible for bootstrapping the local realm and flagging to
ChatBubbleView
that it's a preview:As mentioned at the beginning of this article, the error messages for failed previews are actually useful in Xcode 13.
That's the good news.
The bad news is that you still can't use breakpoints or print to the console.
One mitigation is that the
previews
static var in your preview is a View
. That means that you can replace the body
of your ContentView
with your previews
code. You can then run the app in a simulator and add breakpoints or print to the console. It feels odd to use this approach, but I haven't found a better option yet.I've had a mixed relationship with SwiftUI previews.
When they work, they're a great tool, making it quicker to write your views. Previews allow you to unit test your views. Previews help you avoid issues when your app is running in dark or landscape mode or on different devices.
But, they require effort to build. Prior to Xcode 13, it would be tough to justify that effort because of reliability issues.
I believe that Xcode 13 is the tipping point where the efficiency and quality gains far outweigh the effort of writing preview code. That's why I've written this article now.
In this article, you've seen a number of tips to make previews as useful as possible. I've provided four view builders that you can copy directly into your SwiftUI projects, letting you build the best previews with the minimum of code. Finally, you've seen how you can write previews for views that work with data held in a database such as Realm or Core Data.