Understanding Data Flow in SwiftUI
In this tutorial, you’ll learn how data flow in SwiftUI helps maintain a single source of truth for the data in your app. By Keegan Rush.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Understanding Data Flow in SwiftUI
30 mins
- Getting Started
- The Problem With State
- SwiftUI to the Rescue
- Working With Internal State
- Property Wrappers in SwiftUI
- Reference and Value Types
- Observing Objects
- Why @State Doesn’t Work Here
- A Single Source of Truth
- Passing State as a Binding
- Setting a Favorite Genre
- Working With External Data
- Observing an ObservableObject
- Using the Environment
- Using an Environment Object Down the Chain
- Environment Variables
- Observing Objects
- Choosing the Right Property Wrapper
- Where to Go From Here?
Observing Objects
To update the view when movieStore
changes, you need to observe it. Adding the appropriate property wrapper helps you manage the UI update with the data change. Still in MovieList.swift, add the @ObservedObject
property wrapper to movieStore
, so it looks like this:
@ObservedObject var movieStore = MovieStore()
An observed object is a reference to a class that is observable. Using the @ObservedObject
property wrapper notifies your view every time the observed object updates, so that the view updates for each change.
It works both ways, too. If you bind a text field to a property on an observed object, updating the text field will update the property on the observed object.
The reason that you can observemovieStore
is that it is observable. This works because MovieStore
conforms to the ObservableObject
protocol.
Once again, build and run. Add a new movie. Hooray! The list updates automatically :]
Why @State Doesn’t Work Here
You may be wondering why you couldn’t use @State
for movieStore
.
@State
is only for value types, so you can’t use it on movieStore
which is a reference type. @ObservedObject
allows you to respond to changes like @State
does, but it has one difference.
Every time your data triggers an update in a view, SwiftUI recalculates the view’s body
, thereby refreshing the view. When this happens, if you have an @ObservedObject
declared in your view, SwiftUI recreates that object when your view refreshes.
State variables behave differently. When you use @State
, SwiftUI takes control of the lifecycle of that property. It keeps the value of a @State
property even when the view refreshes.
Fortunately, @State
comes with a companion for reference types: @StateObject
, which works exactly as @State
, but specifically for reference types.
Once again, replace the declaration of movieStore
with this:
@StateObject var movieStore = MovieStore()
Build and run. You’ll see the same behavior as before, but now, SwiftUI manages the lifecycle of movieStore
.
If you’re trying to decide whether to use @State
, @StateObject
or @ObservedObect
, there are two questions you can ask about the property you’re declaring:
-
Is it a value type or a reference type? If you’re working with a value type, use
@State
. -
Should SwiftUI manage the lifecycle of the property? If you’re only using the object in the view the property is declared in,
@StateObject
works fine. But, if the object is created or used outside the view, then@ObservedObject
is a better match.
A Single Source of Truth
SwiftUI data flow is based on the concept that data has a single source of truth. With a single source of truth, there is one and only one place that determines the value of a piece of data.
For example, in UserView
, you set the user’s name. There should only be one place in the code, one source of truth, that determines the value of the user’s name. Any other component that needs that username should refer to the source of truth. This keeps everything in sync so that when the source of truth changes, all its references change too.
Next, you’ll add a new view that illustrates this concept. Every movie has a genre, so wouldn’t it be convenient if the user could have a favorite genre? Then, when adding a new movie, this favorite genre will be the first suggestion.
Open AddMovie.swift. The AddMovie
view has a picker that lets the user pick a genre for the movie. This picker would be perfect to reuse in UserView
to set a favorite genre. But you won’t copy-paste the code from one view to another! Instead, you’ll create a reusable view.
Create a new view in Xcode by going to File ▸ New ▸ File… in the menu bar. Make sure you select SwiftUI View and click Next. Name it GenrePicker and click Create. Replace the contents of your new view with this:
import SwiftUI
struct GenrePicker: View {
// 1
@Binding var genre: String
var body: some View {
// 2
Picker(selection: $genre, label: Spacer()) {
// 3
ForEach(Movie.possibleGenres, id: \.self) {
Text($0)
}
}
.pickerStyle(WheelPickerStyle())
}
}
struct GenrePicker_Previews: PreviewProvider {
static var previews: some View {
// 4
GenrePicker(genre: .constant("Action"))
}
}
In the code above, you took the genre picker from AddMovie
and put it into a reusable view. But one thing has changed: the addition of the @Binding
property wrapper on genre
.
Here’s what’s going on:
- The selected genre is stored in the
genre
property, annotated with the@Binding
property wrapper. - You create a picker wheel. When the user changes the selected row of the picker, it’ll set the
genre
, and vice-versa — settinggenre
changes the picker’s selected row. - Rows in the picker are created by iterating over
Movie.possibleGenres
and displaying each value in aText
view. - You need to pass in a value for
genre
in the preview, but passing a regular string won’t cut it. You need aBinding
. Since it’s a preview, you can create a binding that does nothing with.constant
.
A state property is a source of truth, while a binding is a reference to another property — usually a state property declared elsewhere. A binding lets you reference and update its source of truth.
The intention of GenrePicker
is to let the user select a favorite genre so you can preset that genre in AddView
or anywhere else you choose to use GenrePicker
. This means GenrePicker
doesn’t own the genre it’s setting, so you use @Binding
.
Passing State as a Binding
Now that you have your GenrePicker
ready, it’s time to put it to use.
Open AddMovie.swift again. Find the Section
with the text title “Genre”. Replace its content with:
GenrePicker(genre: $genre)
This replaces the Picker
with your new GenrePicker
. Here in AddMovie
, genre
is a state property. By passing it into GenrePicker
as $genre
, you’re actually passing a binding to the property. Whenever the user modifies the genre inside GenrePicker
, it’ll change the value of AddMovie
‘s genre
.
Build and run. Your picker should look like it did before, but now it’s reusable.