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

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()