Previewing Stateful SwiftUI Views
Interactive Previews for your SwiftUI views
When building UIs in SwiftUI, we tend to build two kinds of UI components: screens and (reusable) views. Usually, we start by prototyping a screen, which will inevitably result in a Massive ContentView that we then start refactoring into smaller, reusable components.
Let’s assume we’re building a todo list application. Here is how it might look like:
I’ve simplified this, but you can imagine that the code for a todo list application like Apple’s Reminders app looks a bit more complicated (in fact, if you’re curious, check out MakeItSo, a sample project I created to replicate the Reminders app using SwiftUI and Firebase).
One way to simplify this code is to refactor the List
view and extract the row into a separate reusable component:
Since TodoRowView
is now a child of the List
view, we want TodoListView
to be the owner of the data. To make sure any changes the user makes (by clicking on the toggle inside the TodoRowView
get reflected on the List
(and vice versa), we need to set up a bi-directional data binding. The right property wrapper for this job is @Binding
- it lets us connect to data that is owned by another view.
The Problem
However, when trying to set up the PreviewProvider
for this view, we quickly run into some limitations: how can we set up the preview with some demo data so that it can be edited inside the TodoRowView
?
We might start by creating a static variable on the PreviewProvider
, and then pass it to the view we want to preview. However, this will inevitably result in a compiler error, as TodoRowView
expects todo
to be a Binding<Todo>
:
Marking the static todo
variable as @State
resolves the compiler error, but it doesn’t result in an interactive preview:
The go-to solution
The usual way to solve this is to use a constant binding. Apple provides us with a static function on Binding
that makes this straightforward:
As the documentation states, this creates a binding with an immutable value, which prevents the underlying property from being updated, so our implementation might seem broken when we run it in the preview pane.
Using a custom binding
Another strategy for dealing with this situation is to use a custom binding. Here is a static function mock
that stores a value in a local variable and provides read/write access to it.
The nice thing about this technique is that is allows us to replace any calls to .constant
with a call to .mock
:
However, this solution only works partially. While it is now possible to flip the toggle, the rest of the UI doesn’t update: when a todo is marked as complete, its title should be striked through, but as you can see in the animation, this doesn’t work.
The Real Solution
It helps to remind ourselves that we have full control over the preview - for example, adding .preferredColorScheme(.dark)
to a view inside the PreviewProvider
will turn on dark mode.
With this in mind, the solution becomes obvious: wrap the view inside a container view, and hold the state in this container view:
This is the solution recommended by Apple in their WWDC 2020 session Structure your app for SwiftUI previews (see this timestamp). Since the container view owns the data via the @State
property wrapper, this approach gives us a SwiftUI preview that is fully interactive and responds to any changes of the view’s state.
One more thing
We can even make this easier by creating a generic container view that passes a binding to a value to its contained view:
This allows us to implement a stateful preview with just a few lines of code:
Here is a gist with the code for the StatefulPreviewContainer
, including a sample for how to use it.
Conclusion
SwiftUI previews are a great tool for developing SwiftUI views, and when they work, they provide an amazing developer experience: no need to re-run the application for every little change you make. This significantly reduces turn-around times, and results in a much more efficient workflow.
It feels a bit like an oversight that Apple didn’t provide an easy way to preview views that connect with their host views view @Binding
, but fortunately, there are some ways around this shortcoming.
In this article, I showed you a couple of approaches that you can use when your SwiftUI views make use of @Binding
to communicate with the outside world. Personally, I like the preview container view the most, and you can even make this more efficient by defining an Xcode code snippet.
SwiftUI Hero Animations with NavigationTransition
Replicating the App Store Hero Animation
Styling SwiftUI Views
How does view styling work?
Asynchronous programming with SwiftUI and Combine
The Future of Combine and async/await
Building a Custom Combine Operator for Exponential Backoff
Make your Combine code reusable
Error Handling with Combine and SwiftUI
How to handle errors and expose them to the user