How do you pass data between views in SwiftUI? If you’ve got multiple views in your SwiftUI app, you’re gonna want to share the data from one view with the next. We’re going to discuss 4 approaches to do so in this tutorial!
Here’s what we’ll get into:
@State
and @Binding
@ObservedObject
and @StateObject
Ready? Let’s go.
This tutorial is the SwiftUI counterpart to my original UIKit-based Pass Data Between View Controllers tutorial. Give both a try, and compare the approaches!
The simplest approach to share data between views in SwiftUI is to pass it as a property. SwiftUI views are structs. If you add a property to a view and don’t provide an initial value, you can use the memberwise initializer to pass data into the view. Let’s take a look at an example!
Consider this simple struct called Book
:
struct Book {
var title: String
var author: String
}
This is the view we want to pass data into:
struct BookRow: View
{
var book: Book
var body: some View {
VStack {
Text(book.title)
Text(book.author)
}
}
}
See how the BookRow
view has a property book
of type Book
? This property does not have an initial value. As a result, an initializer init(book:)
is automatically created for us; it’s the memberwise initializer.
Here’s how to use it:
let books = [···]
List(books) { currentBook in
BookRow(book: currentBook)
}
In the above code, which is part of a SwiftUI view, we’re creating a List view. This List
iterates over the books
array, creating a BookRow
view for each Book
object in the array.
Inside the closure, for the list row’s content, we have access to the “current book” in the iteration. This Book
object is passed into the BookRow
view, as a parameter for its initializer. That’s all there is to it!
What’s so special about this approach? Nothing, really. And that’s exactly what makes it so powerful! You may be tempted to find a clever worflow for sharing data between SwiftUI views, but the simplest approach is often the best.
Advantages:
Disadvantages:
SwiftUI uses bindings to create a connection between a view or UI component, like a Toggle
, and some data, like a boolean isOn
. When the value of isOn
changes, so does the state of Toggle
(“switches on”). It also works the other way: if you flick the switch, the value of isOn
changes accordingly.
Here’s an example:
struct LivingRoom: View
{
@State private var lightsAreOn = false
var body: some View {
Toggle(isOn: $lightsAreOn) {
Text("Living room lights")
}
}
}
Note: Just as in the previous section, the data is passed into the view using the initializer! This is the starting point for (almost) any sharing of data between views; the rest is up to property wrappers, Combine, modifiers, etcetera.
In the above code, we’ve created a view with a Toggle
UI element. It has a label, “Living room lights”, and is passed a binding $lightsAreOn
. This binding is the projected value for the lightsAreOn
property, which is automatically added to it by the @State property wrapper.
It’s easiest to see this as a connection between the lightsAreOn
property and the Toggle
view. Whenever the state of one changes, so does the other. When you flick the toggle to “On”, the value of lightsAreOn
becomes true
, and vice-versa.
Now that the lightsAreOn
property is marked with @State
, the view that this code is a part of will become dependant on the state of lightsAreOn
. In other words, when the value of lightsAreOn
changes, the view will update to represent the new state. This is a core principle of SwiftUI; state drives the UI.
Here’s some code to demonstrate that:
@State private var lightsAreOn = false
···
VStack {
Toggle(isOn: $lightsAreOn) {
Text("Living room lights")
}
Text(lightsAreOn ? "Lights are on!" : "Lights are off")
}
In the above code, the value of lightsAreOn
is used to put some text in a Text
view. Because of @State
, whenever that boolean changes, the view reloads and the appropriate string is subsequently shown in the Text
view.
Internally, the Toggle
view has a property of type Binding<Bool>
. It could look something like this:
@Binding var isOn: Bool
You don’t pass a boolean to Toggle
, but you pass a binding to a boolean to Toggle
. It’s the binding we get from @State
, with $lightsAreOn
. In other words, some data is shared between Toggle
and the LivingRoom
view because of the binding.
You can bind to property wrappers that expose a binding through their projected value. For example, every property marked with @State
provides a binding via $property name
.
You can, of course, create your own views and properties that use the @Binding property wrapper. Using @Binding
to share data between views that way is quite powerful, because you and observe and change data without actually owning that data.
Bindings are available on properties marked with @ObservedObject
, @StateObject
, @EnvironmentObject
, and more. Property wrappers like @ObservedObject
don’t provide bindings to the object itself, but rather, to the properties of that object. You can find an example of binding to properties of a @ObservedObject
in this tutorial.
Advantages:
@Binding
uses data from elsewhere, which is insightful and cleanToggle
or TextField
@State
, provide a binding@Binding
, to split up viewsDisadvantages:
@ObservedObject
, can be confusingSo far, we’ve looked at passing a single piece of data into views (and back) with @State
and @Binding
, and by using properties on views. But what if you want to share the same object with multiple views? That’s where @EnvironmentObject
comes in.
In short, the @EnvironmentObject
property wrapper and its .environmentObject(_:)
modifier enable you to insert objects into the “environment” of a view.
Think of environment as a space for data, separate to the view. This environment is shared between views and their descendants (subviews), which makes it perfect for passing an object down into a hierarchy of views.
Let’s take a look at an example:
struct DetailHeader: View
{
@EnvironmentObject var book: Book
var body: some View {
VStack {
Text(book.title)
Text(book.author)
}
}
}
In the above code, we’ve created a DetailView
struct. It’s got 2 simple Text
views that display some data from a Book
class. That object comes from a property book
, which is wrapped by @EnvironmentObject
.
When the DetailHeader
view is shown in your app, SwiftUI will look for an object of type Book
in the view’s environment and assign it to the book
property.
Here’s the Book
class we’re using:
class Book: ObservableObject {
@Published var title: String
@Published var author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
Just as before, it’s got 2 properties title
and author
. But unlike before, Book
is a class now. It conforms to the ObservableObject
protocol, and the 2 properties are marked with @Published
. In short, this means that any changes to an instance of Book
will now be emitted to subscribers, such as with @ObservedObject
, which subsequently updates its view.
The view hierarchy for this app is as follows:
BookApp
struct
DetailView
struct
DetailHeader
structAt the top level, we’ve got this App
struct:
@main
struct BookApp: App
{
var book = Book(title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams")
var body: some Scene {
WindowGroup {
DetailView()
.environmentObject(book)
}
}
}
See the Book
object? That’s the data we’re sharing with the DetailView
, and its descendants, by using the environmentObject(_:)
modifier. The Book
object is passed into the view, and is shared with all its subviews.
Here’s the DetailView
:
struct DetailView: View
{
var body: some View {
VStack {
DetailHeader()
Text("Lorem ipsum dolor sit amet")
}
}
}
If you look closely, you’ll notice that the Book
object is injected into the environment at the top-level. The mid-level DetailView
does nothing with it, and yet the object is passed into the DetailHeader
view at the low-level. How is that possible?
Sharing data between views using @EnvironmentObject
takes 2 steps:
.environmentObject(_:)
@EnvironmentObject
Because the environment is shared between views, you can grab any object from the environment, even if it’s a few descendants down. In this example, we’re only grabbing the shared object once, but you can get the same object from the environment from any number of views.
It doesn’t matter if you “skip over” a subview; the object’s there in the environment. That makes using @EnvironmentObject
to share objects between views quite useful, because you don’t have to manually pass down an object, that you want to use multiple times, into each subview.
You can only pass one object per type into the environment, so we cannot pass 2 Book
objects. When you do this, the first object passed with .environmentObject(_:)
will be used. If you need to pass multiple objects, consider organizing your data models better, ex. use a view model with an array.
Advantages:
ObservableObject
, with all its benefits (see below)Disadvantages:
ObservableObject
must be a class@EnvironmentObject
for everything, much like singletonsThe @EnvironmentObject
property wrapper is similar to @Environment
(without “Object”) and .environment(_:_:)
. You use them to set objects based on the keys from EnvironmentValues. This allows you to get/set common values in the environment, such as .managedObjectContext
, accessibility features, color scheme, locale, or the presentation mode.
Last but not least! The mighty ObservableObject
protocol. This approach to share data between SwiftUI is the most complex, and the most powerful.
It affects 3 different property wrappers:
@StateObject
@EnvironmentObject
@ObservedObject
In short, you use objects that conform to the ObservableObject
protocol, in combination with the 3 property wrappers above, to observe and publish data changes to views that depend on that data. You can also bind to their properties.
Here’s an example:
struct DetailView: View
{
@ObservedObject var book: Book
var body: some View {
VStack {
Text(book.title)
Text(book.author)
}
}
}
In the above code, we’ve marked the book
property with the @ObservedObject
property wrapper. We’re using the same Book
class as before, so it conforms to ObservableObject
and publishes changes for properties marked with @Published
.
You pass a Book
object to the DetailView
as you would any other property:
var book = Book(title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams")
···
DetailView(book: book)
When the data for the Book
object changes, the DetailView
will update itself because it depends on that object. The Book
object is pretty static in the above code; you use @ObservedObject
for data that’s created and changed elsewhere in your code.
We’ve already touched upon the @EnvironmentObject
property wrapper and the ObservableObject
protocol before, and then there’s @StateObject
too. They’re actually closely related to the @ObservedObject
property wrapper.
A brief overview:
ObservableObject
protocol for a class, whose properties can be observed (if marked with @Publisher
)@StateObject
for reference types (i.e., classes), whose objects are initialized locally in a view@EnvironmentObject
for reference types that are inserted into the environment via the .environmentObject(_:)
modifier@ObservedObject
for reference types, whose objects are created externally to a viewWe’ve already discussed @EnvironmentObject
; which deals with the view’s environment. You use @StateObject
for objects that are created locally in a view, like this:
struct NewBookView: View
{
@StateObject var book = Book()
var body: some View {
VStack {
TextField("Title", text: $book.title)
TextField("Author", text: $book.author)
}
}
}
In the above code, we’re initializing a Book
object locally inside the NewBookView
. We’re using bindings to connect the TextField
with the properties from the book
object, which will set their values when the text field changes (and vice-versa).
You can still pass this book
object to other views with the @ObservedObject
property wrapper, but the source of truth will be the @StateObject
. In that sense, the @StateObject
property wrapper is a mix between @State
and @ObservedObject
. Awesome!
Advantages:
ObservableObject
is as custom and flexible as it gets@StateObject
or @ObservedObject
@Published
is a sensible segway into using Combine@StateObject
, @ObservedObject
and @EnvironmentObject
Disadvantages:
ObservableObject
classLooking for an in-depth guide on working with @ObservedObject
? Check out this tutorial: @ObservedObject and Friends in SwiftUI
You could argue that, once you forget about building User Interfaces, sharing data is all that SwiftUI does. You’ve got state and UIs, and that’s it!
So it shouldn’t come as a surprise that SwiftUI has syntax and tools that make sharing data between views easier, but it isn’t always clear what approach you should use. In this tutorial, we’ve discussed 4 approaches to pass data between views.
Here they are once more:
@EnvironmentObject
@Binding
, and how to get that binding@ObservedObject
(and alternatives)Want to learn more? Check out these resources: