Realm
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
Realmchevron-right

Making SwiftUI Previews Work For You

Andrew MorganPublished Oct 29, 2021 • Updated May 13, 2022
RealmSwift
Copy Link
facebook icontwitter iconlinkedin icon
random alt
Rate this article
star-empty
star-empty
star-empty
star-empty
star-empty

Introduction

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.
Xcode showing a blackjack app with multiple previews for different devices, orientations, and color schemes
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.

Prerequisites

  • 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.

Working with previews

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.
A super-simple preview
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).
A simple Xcode preview which updates as the code is edited
The preview code always has the same structure, with the View that needs previewing (in this case, ContentView) within the previews View:
Views that require parameters
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.
My ResetButton view requires that the caller provides two values—label and resetType:
The preview code needs to pass in those values, just like any embedding view:
Views that require
Binding
s
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:
NavigationView
s
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.
If I preview my PracticeView without adding a NavigationView, then I don't see the title:
Preview where there is no title on the screen
To preview the title, my preview code needs to wrap PracticeView in a NavigationView:
Preview with title
Smaller views
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:
Lots of preview space taken up for the iPhone, but the view we're testing only takes up a tiny proportion of the 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:
Lots of screen space freed up by only previewing the view (rather than the entire iPhone screen
Light and dark modes
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:
10 Hearts displaying correctly in front of a dark background
That seems fine, but what if I previewed a spade instead?
Black spade isn't visible in front of dark background
That could be a problem.
Adding a white background to the view fixes it:
King of spades now shows up as it sits in a white box
Preview multiple view instances
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:
Multiple cards, each with their own preview
Composing views in a preview
A preview of a single view in isolation might look fine, but what will they look like within a broader context?
Previewing a single DecisionCell view looks great:
A single, yellow Hit cell
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 DecisionCells using vertical and horizontal stacks:
I could then see that the black border didn't work too well in dark mode:
Dark border around selected cells is lost in front of the dark background
Switching the border color from black to primary quickly fixed the issue:
White frame around selected cell is visible in front of dark background
Landscape mode
Previews default to portrait mode. Use the previewInterfaceOrientation modifier to preview in landscape mode instead:
iPhone app previewed in lanscape mode
Device type
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:
App previewed on an iPad
You can find the names of the available devices from Xcode's simulator menu, or from the terminal using xcrun simctl list devices.
Pinning views
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:
Pin icon in Xcode
This is useful to observe how a parent view changes as you edit the code for the child view:
Animation of the pinned view continuing to be shown and updates as the developer switches to other views in Xcode
Live previews
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:
Play button above Xcode preview
You can now interact with your view:
Preview responds to user actions

Getting rid of excess boilerplate preview code

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.
Light/dark mode
_PreviewColorScheme returns a Group of copies of the view. One is in light mode, the other dark:
To use this view builder in a preview, simply pass in the View you're previewing:
multiple cards shown in the same preview
Orientation
_PreviewOrientation returns a Group containing the original View in portrait and landscape modes:
To use this view builder in a preview, simply pass in the View you're previewing:
Preview showing the same view in portrait and lanscape modes
No device
_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:
9 Clubs previewed in a small window
Multiple devices
_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:
The same view previewed on 3 different device types
Combining multiple view builders
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 _PreviewColorScheme, but previewing on different devices and orientations wouldn't add any value.

Previewing stateful views (Realm)

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:
Partitioned, synced realms
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.
AuthorView from the RChat app fetches data from Realm:
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:

Troubleshooting your previews

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.

Conclusion

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.
Please provide feedback and ask any questions in the Realm Community Forum.

Copy Link
facebook icontwitter iconlinkedin icon
Rate this article
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Article
Document our Realm-Powered Swift Frameworks using DocC

May 13, 2022
Tutorial
OAuth & MongoDB Realm Serverless Functions

Sep 23, 2022
Tutorial
Persistence in Unity Using Realm

Sep 07, 2022
Tutorial
Realm Data Types

May 09, 2022
Table of Contents