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:
struct TodoListView: View {
@State var todos = [Todo]()
var body: some View {
NavigationStack {
List($todos) { $todo in
Toggle(isOn: $todo.completed) {
Text(todo.title)
.strikethrough(todo.completed)
}
}
.listStyle(.plain)
.navigationTitle("My tasks")
}
}
}
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:
struct TodoRowView: View {
@Binding var todo: Todo
var body: some View {
Toggle(isOn: $todo.completed) {
Text(todo.title)
.strikethrough(todo.completed)
}
}
}
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>
:
struct TodoRowView_Previews: PreviewProvider {
static var todo = Todo(title: "Draft article", completed: false)
static var previews: some View {
TodoRowView(todo: todo)
// ^ error: "Cannot convert value of type 'Todo' to expected argument type 'Binding<Todo>'"
}
}
Marking the static todo
variable as @State
resolves the compiler error, but it doesn’t result in an interactive preview:
struct TodoRowView_Previews: PreviewProvider {
@State static var todo = Todo(title: "Draft article", completed: false)
static var previews: some View {
TodoRowView(todo: $todo)
}
}
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:
struct TodoRowView_Previews_withConstantBinding: PreviewProvider {
static var previews: some View {
TodoRowView(todo: .constant(Todo.sampple))
}
}
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.
extension Binding {
static func mock(_ value: Value) -> Self {
var value = value
return Binding(get: { value },
set: { value = $0 })
}
}
The nice thing about this technique is that is allows us to replace any calls to .constant
with a call to .mock
:
struct TodoRowView_Previews_withMockBinding: PreviewProvider {
static var previews: some View {
TodoRowView(todo: .mock(Todo.sampple))
}
}
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:
struct TodoRowView_Previews_Container: PreviewProvider {
struct Container: View {
@State var todo = Todo.sampple
var body: some View {
TodoRowView(todo: $todo)
}
}
static var previews: some View {
Container()
}
}
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:
struct StatefulPreviewContainer<Value, Content: View>: View {
@State var value: Value
var content: (Binding<Value>) -> Content
var body: some View {
content($value)
}
init(_ value: Value, content: @escaping (Binding<Value>) -> Content) {
self._value = State(wrappedValue: value)
self.content = content
}
}
This allows us to implement a stateful preview with just a few lines of code:
struct TodoRowView_Previews_withGenericWrapper: PreviewProvider {
static var previews: some View {
StatefulPreviewContainer(Todo.sampple) { binding in
TodoRowView(todo: binding)
}
}
}
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.
![](/_astro/gemini-genkit-pdf-structured-data.DrOAWBRa_Z1y23Vu.webp)
Extracting structured data from PDFs using Gemini 2.0 and Genkit
![](/_astro/swiftui-preferences-swift6.C6fNZ7DO_Z1QsQIT.webp)
Understanding SwiftUI Preferences
SwiftUI Parent / Child View Communication
![](/_astro/swiftui-action-menu.Dviry5Lp_kfQrI.webp)
Creating a reusable action menu component in SwiftUI
![](/_astro/custom-sf-symbols.D3c5LWln_ZFa9MC.webp)
Creating custom SF Symbols using the SF Symbols app
No Design Skills Needed
![](/_astro/delay-task-modifier.D4e7rqQq_ZWmN07.webp)
Improve your app's UX with SwiftUI's task view modifier
Mastering the art of the pause