All videos. All books. One low price.

Get unlimited access to all video courses and books on this site with the new raywenderlich.com Ultimate Subscription. Plans start at just $19.99/month.

Home iOS & Swift Tutorials

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.

4.9/5 9 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

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 app listing three movie titles and ratings

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.

Add Movie screen

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.

Different States

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.

Empty User View

This screen is a bit empty as of now. You’ll want it to:

  1. Show a text field that lets you add your name.
  2. 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:

  1. Stores the username in a state property. More on what this means later.
  2. Passes userName as a binding to a TextField. When the user types into the text field, the text field updates userName. This binds the TextField to userName.
  3. Displays userName as part of the navigation bar. Whenever userName changes, the navigation bar title updates.
  4. 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à! :]

Username updating

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.

Populated add screen

When you’re done, tap Add. You return to the movie list, but your new movie is nowhere to be found.

New movie missing

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.

New movie appears in the list of favorite movies

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

New Movie Shows Instantly

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.

A view of all the added movies

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:

  1. The selected genre is stored in the genre property, annotated with the @Binding property wrapper.
  2. You create a picker wheel. When the user changes the selected row of the picker, it’ll set the genre, and vice-versa — setting genre changes the picker’s selected row.
  3. Rows in the picker are created by iterating over Movie.possibleGenres and displaying each value in a Text view.
  4. You need to pass in a value for genre in the preview, but passing a regular string won’t cut it. You need a Binding. 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.

Add Movie screen with a reusable picker

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.

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.

List after User View Update

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.

Setting a favorite genre

Now, go to the Add Movie screen. You’ll see your favorite genre as the selected choice in the picker. Hooray!

Viewing Favorite Genre

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:

Data Flow Decision Chart

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!

Average Rating

4.9/5

Add a rating for this content

9 ratings

More like this

Contributors

Comments