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?
Changing data in a SwiftUI view can make your UI update automatically. It almost seems like magic. But there’s no magic here, only a sleight of hand known as data flow.
Data flow is one of the key pillars of SwiftUI. Define your UI, give it some data to work with, and for the most part, it just works. Everything stays in sync, and there’s no risk of the UI ending up in a bad state due to changing data.
To effectively use SwiftUI, you need to understand the concept of data flow. In this tutorial, you’ll work on an app named Fave Flicks. You’ll learn the different ways to manage data flow and how to add new features to the app safely.
More specifically, you’ll learn about:
- Different data flow property wrappers.
- Managing sources of truth.
- Handling data created by your view.
- Passing external data to your view.
- Sharing data between views.
By the time you’re done, you’ll feel much more comfortable putting data into your views in SwiftUI.
Getting Started
First, download the project materials using the Download Materials button at the top or bottom of this tutorial. Then, open the starter project in Xcode. Build and run.
Fave Flicks is a personalized list of movies you love. At the moment, it consists of a basic list of movies with a separate screen for adding new entries.
Are you excited to save your favorite movies? Soon you’ll incorporate the magic of data flow to your code to add new features and complete the app. :]
The Problem With State
There are a couple of good reasons Apple decided to build SwiftUI, one of which relates to the management of state. To illustrate this, here are some scenarios that show how managing different states becomes a problem when you have to manually update your view.
With UIKit, it’s up to you to update your UI when data changes. For example, whenever you add a new movie to Fave Flicks, you want it to appear in the list of movies on the main screen. Ensuring this happens isn’t difficult, but it’s easy to forget something when your app grows with new features.
In addition to keeping the UI data up to date, you might need to show a loading spinner while the view loads. If the movie list is empty, you can show a message to prompt the user to add something. If there’s an error, you can hide everything else and show an error message. You have to do this every time your data changes.
This series of manual updates — where you write code to update the UI, adding or removing bits and pieces as data changes — is known as imperative programming. Relying on this paradigm leads to complex and bloated view controllers, a problem in iOS development.
SwiftUI to the Rescue
In place of the imperative approach, SwiftUI adopts a declarative programming style that makes updating your UI a breeze. Rather than focusing on how to update the UI, you focus on what to update. SwiftUI’s data flow handles the rest.
Every time your data changes, SwiftUI rebuilds any views depending on that data from scratch according to the latest data. This means there’s no risk of displaying a loading spinner, a table of results and an error message all at once.
Later, you’ll see how declaring your UI and using data flow will make your app respond to data exactly how you want it to.
Working With Internal State
For the first new feature, set up your profile page. This is where you can set your name and favorite movie genre. Build and run. Tap the user icon on the left side of the navigation bar.
This screen is a bit empty as of now. You’ll want it to:
- Show a text field that lets you add your name.
- Change your name, which also updates navigation bar title.
Open UserView.swift. Replace the contents of the file with the following:
import SwiftUI
struct UserView: View {
// 1
@State private var userName = ""
var body: some View {
NavigationView {
Form {
Section(header: Text("User")) {
// 2
TextField("User Name", text: $userName)
}
}
}
// 3
.navigationBarTitle(Text("\(userName) Info"), displayMode: .inline)
.navigationBarItems(
trailing:
Button(action: updateUserInfo) {
Text("Update")
}
)
}
// 4
func updateUserInfo() {
}
}
The code above:
- Stores the username in a state property. More on what this means later.
- Passes
userName
as a binding to aTextField
. When the user types into the text field, the text field updatesuserName
. This binds theTextField
touserName
. - Displays
userName
as part of the navigation bar. WheneveruserName
changes, the navigation bar title updates. - Calls
updateUserInfo()
when the user taps Update in the navigation bar. You’ll add content to it later.
Build and run. Tap the user button in the navigation bar again, and you’ll see your new view! Type into the text field and watch as it updates the navigation bar title with each character you enter. Voilà! :]
This magic is driven by the @State
property wrapper you added in front of userName
. It means SwiftUI itself owns and manages userName
. Every time you change the value of a state property, SwiftUI recreates the views depending on that state so they’re always up to date.
Property Wrappers in SwiftUI
At this point, you may be wondering how @State
changes the behavior of userName
. @State
is a type of property wrapper that updates your UI whenever the value of the property bound to it changes.
When you see the @
symbol when declaring a property, this means it’s using a property wrapper. You control data flow in SwiftUI by using the associated data flow property wrappers:
@State
@Binding
@StateObject
@ObservedObject
@EnvironmentObject
@Environment
Reference and Value Types
@State
properties are only useful for value types like String
, Int
, Bool
, struct
and enum
. If you want the view to own a reference type, such as a class, you use @ObservedObject
or @EnvironmentObject
instead. You’ll learn about these later in this tutorial.
Build and run. Tap the + button in the top-right corner to reach the Add Movie view. Give it a title, select a genre and add your rating.
When you’re done, tap Add. You return to the movie list, but your new movie is nowhere to be found.
Don’t worry. You successfully created an entry, but it isn’t showing yet.
Open MovieList.swift. You’ll see movieStore
defined at the top of the view. This array keeps track of all the movies you’ve added or deleted. Build and run again and your new movie will appear as expected.
The problem here is that the view doesn’t know to update automatically when you add a new movie. Nothing tells it to fetch the latest list of movies from movieStore
. That’s because you declared movieStore
as a plain old property:
var movieStore = MovieStore()
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.
Setting a Favorite Genre
Open UserView.swift. At the top of the view, add this line after the one which defines userName
:
@State private var favoriteGenre = ""
Next, inside the Form
, add a new Section
after the existing one:
Section(header: Text("Favorite Genre")) {
GenrePicker(genre: $favoriteGenre)
}
This uses your GenrePicker
to set the user’s favorite genre.
Build and run. You’ll see a picker on the user view just like the one on the Add Movie screen. When GenrePicker
‘s value changes, it’ll update favoriteGenre
.
If you leave UserView
and come back to it though, you’ll notice your selection isn’t persisted. You’ll take care of this in just a moment.
Working With External Data
Currently, UserView
sets a username and a favorite genre using @State
. You want to be able to pass these elsewhere in the app so next, you’ll create a UserStore
class that keeps track of the current user’s data.
Create a new view in Xcode by going to File ▸ New ▸ File… in the menu bar. Select Swift File and click Next. Name it UserInfo.swift and click Create. Replace the contents with this:
import Foundation
struct UserInfo {
let userName: String
let favoriteGenre: String
}
UserInfo
is a Swift struct that represents a user. Next, create another file in the same manner. Name this one UserStore.swift. Replace its contents with the following:
import Combine
// 1
class UserStore: ObservableObject {
// 2
@Published var currentUserInfo: UserInfo?
}
UserStore
keeps track of the current user by using the UserInfo
struct you declared earlier. Thanks to the Combine framework and property wrappers, there’s a lot of new stuff happening in the code above:
-
UserStore
conforms toObservableObject
. This is something that can be observed by SwiftUI. - The
@Published
property wrapper is what triggers any updates in observers of anObservableObject
. Whenever a published property changes, observers are notified. By declaringcurrentUserInfo
with the@Published
property wrapper, setting a new value will update any views observing theUserStore
.
Observing an ObservableObject
Sometimes, your data’s source of truth doesn’t live inside a SwiftUI view. In this case, use the ObservableObject
protocol to allow a class to interact with SwiftUI. An ObservableObject
is a source of truth that sends updates to a SwiftUI view, and it receives updates based on changes to the UI.
So far, you’ve created a class conforming to ObservableObject
that you can reference in a SwiftUI view: UserStore
. Next, you need do the observing, which means you’ll need UserStore
in the following places:
- UserView: Sets the username and favorite genre
- MovieList: Displays the user’s name
- AddMovie: Uses the favorite genre as the default
Using the Environment
In SwiftUI, the environment is a store for variables and objects that are shared between a view and its children.
You need a reference to an observable object, and one way to get it is to use @ObservedObject
, as you did for MovieStore
, then pass the reference to UserStore
wherever you need it.
This gets tedious in large apps with many nested views. Because of this, UserStore
is a good candidate for an environment object. Rather than passing an object to every view that needs it, environment objects are supplied by an ancestor view and are made available to any of its descendants.
This means that if you create an instance of UserStore
and pass it to MovieList
as an environment object, all the child views of MovieList
will get UserStore
automatically.
To use an environment object, you need to do the following:
- Create a class conforming to
ObservableObject
. - Have at least one variable in the class with the
@Published
property wrapper to trigger any observers to update, or manually provide anobjectWillChange
publisher as required byObservableObject
. - Pass an instance of the observable class to a view by using the
environmentObject()
view modifier when creating the view.
When you created the UserStore
class, you already accomplished the first two steps. Next, you need to pass UserStore
into MovieList
‘s environment. Open SceneDelegate.swift.
In scene(_:willConnectTo:options:)
, replace the first line that creates contentView
with this:
let contentView = MovieList().environmentObject(UserStore())
After creating the MovieList
, you pass an instance of UserStore
into its environment using environmentObject(_:)
. Now the movie list and the views in its hierarchy can access it.
Next, open MovieList.swift. To access UserStore
, add the following at the beginning of the struct, right before the line declaring movieStore
:
@EnvironmentObject var userStore: UserStore
@EnvironmentObject
lets you use environment objects passed to a view or to any of its ancestor views.
Now you can display the username along with the user navigation item. In MovieList.swift, find the Image
with the person.fill
system icon. Replace that line with this:
HStack {
// 1
userStore.currentUserInfo.map { Text($0.userName) }
// 2
Image(systemName: "person.fill")
}
In the code above, you:
- Get the current user’s
userName
property to display as aText
view if it exists.currentUserInfo
is an optional, but you can’t use anif let
inside a view’s body. Usingmap
will create theText
view for you only ifcurrentUserInfo
exists. - Add the same image that was there before to the
HStack
.
Great work! Build and run. You’re not passing the username to userStore
, so you won’t see any changes.
Using an Environment Object Down the Chain
You don’t need to pass UserStore
to UserView
to gain access to it. The environment already handles that for you. Instead, open UserView.swift and, just as with MovieList
, add userStore
at the top of UserView
, along with the other variables:
@EnvironmentObject var userStore: UserStore
Add the following to the empty updateUserInfo()
:
let newUserInfo = UserInfo(userName: userName, favoriteGenre: favoriteGenre)
userStore.currentUserInfo = newUserInfo
Tapping Update calls updateUserInfo()
. The method creates a new userInfo
object with the two state properties that drive this view: userName
and favoriteGenre
. It then updates the userStore
published property so you can use it elsewhere in the app. UserStore
is an ObservableObject
, so this will trigger updates in all the observers.
At the end of the body
, after the navigationBarItems
view modifier, add another view modifier:
.onAppear {
userName = userStore.currentUserInfo?.userName ?? ""
favoriteGenre = userStore.currentUserInfo?.favoriteGenre ?? ""
}
This grabs the current userName
and favoriteGenre
from the userStore
when the view appears.
Build and run. Tap the user button, give yourself a name, and tap Update. Then tap the left navigation bar button to go back to the movie list. You should see your name next to the user button. Hooray! :]
You might have noticed that tapping Update didn’t navigate back to the user view for you. You’ll add that next.
Environment Variables
In UserView.swift, add this line along with the rest of the property declarations:
@Environment(\.presentationMode) var presentationMode
This new property wrapper, @Environment
, is like @EnvironmentObject
in one way: both are shared across a view hierarchy. The difference is that you can add anything as an environment object, but environment values are more like key-value pairs.
While both @EnvironmentObject
and @Environment
share the environment, they serve very different purposes. You usually use @EnvironmentObject
to manage dependencies in your app. SwiftUI uses @Environment
as a way to manage settings for views and their children. Each view comes with environment values you can use to change the behavior of views in the hierarchy. One of these values is presentationMode
.
Add the following line to the end of updateUserInfo()
:
presentationMode.wrappedValue.dismiss()
The presentation mode is shared across a view hierarchy, which makes it an environment variable. SwiftUI uses the presentation mode to manage what is currently displaying. By calling dismiss()
, the view hierarchy will dismiss the current view, which, in this case, is UserView
.
Build and run. Update your user information and tap Update and see how the app now dismisses the user view.
Observing Objects
To update the default genre when adding a movie, you need to get a reference to UserStore
in AddMovie
.
Open AddMovie.swift. Add the following code near the top of the view, directly under the declaration of movieStore
:
@EnvironmentObject var userStore: UserStore
Next, find the closing brace of the NavigationView
inside AddMovie
‘s body
. It should be the last closing brace before the end of body
. Add the following view modifier:
.onAppear { genre = userStore.currentUserInfo?.favoriteGenre ?? "" }
This sets genre
to the user’s favorite genre, if there is one. GenrePicker
‘s selection then updates to the favorite genre, thanks to data flow.
Finally, build and run. Click the user icon and set a favorite genre for yourself. Click Update to save the changes.
Now, go to the Add Movie screen. You’ll see your favorite genre as the selected choice in the picker. Hooray!
Choosing the Right Property Wrapper
Data flow is a lot to take in at first, and you might be wondering how to choose the right tool for the job.
If you don’t need to observe something to respond to updates, a normal property works fine. If you want to refresh a view any time something changes, then make your decision using these questions:
Where to Go From Here?
Download the final project by using the Download Materials button at the top or bottom of this tutorial.
Congratulations! You’ve mastered the art of data flow in SwiftUI and its associated property wrappers. You learned about the concept of sources of truth, which you can now use to keep all your data up to date. You also identified which property wrapper to use for the correct situation.
If you want to learn more about SwiftUI, including a whole chapter on data flow, then the SwiftUI by Tutorials book has what you need.
You can also refer to Apple’s documentation on State and Data Flow.
Working with data in SwiftUI is easy once you understand data flow, and we hope this tutorial made things clearer. If you have any questions or comments, please join the forum discussion below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more