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.

4.8 (32) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

Favorite Genre Picker

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:

  1. UserStore conforms to ObservableObject. This is something that can be observed by SwiftUI.
  2. The @Published property wrapper is what triggers any updates in observers of an ObservableObject. Whenever a published property changes, observers are notified. By declaring currentUserInfo with the @Published property wrapper, setting a new value will update any views observing the UserStore.

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:

  1. Create a class conforming to ObservableObject.
  2. Have at least one variable in the class with the @Published property wrapper to trigger any observers to update, or manually provide an objectWillChange publisher as required by ObservableObject.
  3. 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:

  1. Get the current user’s userName property to display as a Text view if it exists. currentUserInfo is an optional, but you can’t use an if let inside a view’s body. Using map will create the Text view for you only if currentUserInfo exists.
  2. 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.

Same Old Movie List

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! :]

List with Username

You might have noticed that tapping Update didn’t navigate back to the user view for you. You’ll add that next.