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?
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:
-
UserStoreconforms toObservableObject. This is something that can be observed by SwiftUI. - The
@Publishedproperty wrapper is what triggers any updates in observers of anObservableObject. Whenever a published property changes, observers are notified. By declaringcurrentUserInfowith the@Publishedproperty 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
@Publishedproperty wrapper to trigger any observers to update, or manually provide anobjectWillChangepublisher 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
userNameproperty to display as aTextview if it exists.currentUserInfois an optional, but you can’t use anif letinside a view’s body. Usingmapwill create theTextview for you only ifcurrentUserInfoexists. - 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.