Chapters

Hide chapters

macOS Apprentice

Second Edition · macOS 15 · Swift 5.9 · Xcode 16.2

Section II: Building With SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

Section III: Building With AppKit

Section 3: 6 chapters
Show chapters Hide chapters

7. Data Flow in SwiftUI
Written by Sarah Reichelt

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the last chapter, you created a Game data model and used it to make the Snowman game playable. You imported a data file for generating random words, and you connected the data to all the components of the game view.

In this chapter, you’ll extend your data to include the entire app. This uses a new data object that holds a list of games as well as other data your app needs.

You’ll learn how do set up data classes and their properties and how to pass this data around to the views.

This involves several more property wrappers, so hover your finger over the @ key and get ready.

Creating an App Model

Start Xcode and open your project from the previous chapter, or use the starter project from the downloaded materials for this chapter. Press Command-R to run the app to remind yourself of where you ended:

The starter app
The starter app

The game view is complete, the game is playable and the Game model is functional. The sidebar still only displays placeholder text, so that’s what you’ll add next.

You’ll create a new model class to hold a list of games and the data needed to swap between them.

Select the Models folder in the Project navigator and press Command-N to make a new file. Choose macOS ▸ Source ▸ Swift File and name it AppState.swift.

Your previous models have been structures or enumerations, but this one has to be a class. When you learned about classes and structures, you found that classes are reference types and structures are value types. SwiftUI has definite rules about places where you must use reference types and you’re about to encounter one.

Replace the contents of the new file with:

// 1
import SwiftUI

// 2
@Observable
class AppState {
}

What does this do?

  1. Start by importing the SwiftUI library. You don’t need it yet, but you will. Importing SwiftUI automatically imports Foundation, which is why you can replace the default import.
  2. This model is a class called AppState and it conforms to Observable.

Observable

So what is the Observable protocol? A class that conforms to Observable is a class that publishes changes to its properties. This is commonly used in SwiftUI to indicate that data has changed and to trigger an update of the views.

// 1
var games: [Game]
// 2
var gameIndex: Int
// 3
var selectedID: Int?

// 4
init() {
  // 5
  let newGame = Game()
  games = [newGame]
  
  // 6
  gameIndex = 0
  selectedID = 1
}

Identifying the Game

When you defined the Letter structure, you made it Identifiable so SwiftUI could loop through it using ForEach with a way of distinguishing each letter.

let id: Int
struct Game: Identifiable {
// 1
init(id: Int) {
  // 2
  self.id = id
  // 2
  word = getRandomWord()
}
Build errors
Beimx odpaxc

Game(id: 1)

Adding a State Property

You defined an Observable class, but you haven’t used it yet. This class defines app-wide settings, so you’ll add it to the app itself.

@State var appState = AppState()

Environment Objects

Now that your app has its appState, you can pass it around to the other views. There are two ways to do this, and you’ll learn both. The first one uses @Environment.

.environment(appState)
// 1
@Environment(AppState.self) var appState

// 2
var game: Game {
  appState.games[appState.gameIndex]
}
@Bindable var appStateBindable = appState
GuessesView(game: $appStateBindable.games[appState.gameIndex])
.environment(AppState())
Running the game with the Environment object.
Yilwicd rla lafu tiwx hbe Undufidgobp uvhatm.

Starting a New Game

Next, you need to give AppState a way to create a new game.

// 1
func startNewGame() {
  // 2
  let newGame = Game(id: games.count + 1)
  // 3
  games.append(newGame)

  // 4
  selectedID = newGame.id
  gameIndex = games.count - 1
}
appState.startNewGame()
New game
Fah yave

Populating the Sidebar

Finally, you’re ready to start work on the sidebar, so open SidebarView.swift. As with GameView, you need to give the preview access to an @Environment object so it can use its data.

.environment(AppState())
// 1
@Environment(AppState.self) var appState

// 2
var body: some View {
  // 3
  List(appState.games) { game in
    // 4
    VStack(alignment: .leading) {
      // 4
      Text("Game \(game.id)")
        .font(.title3)
      Text(game.word)
    }
    // 5
    .padding(.vertical)
  }
}
Testing the sidebar.
Piynody pne fipaxiw.

Getting Data for the Sidebar

Right now, the sidebar shows the game header and the word, but you only want the word to appear if the game is over.

// 1
var sidebarWord: String {
  // 2
  if gameStatus == .inProgress {
    return "???"
  }
  // 3
  return word
}
Text(game.sidebarWord)
Hiding the current word in the sidebar.
Dilikg bxe wufrilf hajr ag who hujatug.

Computing Properties

Open GameStatus.swift and start by adding this import at the top:

import SwiftUI
// 1
var displayStatus: Text {
  // 2
  switch self {
  case .inProgress:
    // 3
    return Text("In progress…")
  case .lost:
    // 4
    let img = Image(systemName: "person.fill.turn.down")
    return Text("You lost \(img)")
  case .won:
    // 5
    let img = Image(systemName: "heart.circle")
    return Text("You won! \(img)")
  }
}
Searching the symbols library.
Baummnuyn thi zdvpovs caydozk.

game.gameStatus.displayStatus

Adding Color

Again, GameStatus is the place to do this, so open GameStatus.swift and add this:

var statusTextColor: Color {
  switch self {
  case .inProgress:
    return .primary
  case .won:
    return .green
  case .lost:
    return .orange
  }
}
.foregroundStyle(game.gameStatus.statusTextColor)
Coloring the sidebar
Wugazult rzo hewanic

.foregroundStyle(game.gameStatus.statusTextColor)
Environment Overrides
Icletarmuxg Oniscoxal

Making the Sidebar Live

A List can have a selection parameter. This is an optional value that changes when the user selects or deselects a list item. You already created the optional selectedID property in AppState for this purpose.

// 1
@Bindable var appStateBindable = appState

// 2
List(appState.games, selection: $appStateBindable.selectedID) { game in
func selectGame(id: Int?) {
  // 1
  guard let id else {
    return
  }

  // 2
  let gameLocation = games.firstIndex { game in
    game.id == id
  }
  if let gameLocation {
    gameIndex = gameLocation
  }
}
// 1
var selectedID: Int? {
  // 2
  didSet {
    // 3
    selectGame(id: selectedID)
  }
}
Selecting games from the sidebar.
Vetaprotq zipot bdoh dro quyixez.

Using Array Methods

You’ve seen several uses of array methods like filter and firstIndex. They loop through arrays, but the way they operate can be confusing.

// 1
let names = [ "Alice", "Ben", "Celine", "Danny", "Edith" ]

// 2
var fiveLetterNames: [String] = []
// 3
for name in names {
  // 4
  if name.count == 5 {
    fiveLetterNames.append(name)
  }
}
// 1
let filteredNames = names.filter { name in
  // 2
  name.count == 5
}
// 1
func countEqualsFive(string: String) -> Bool {
  string.count == 5
}
// 2
let filteredNames2 = names.filter(countEqualsFive)
let filteredNames3 = names.filter({ name in
  name.count == 5
})
let filteredNames4 = names.filter {
  $0.count == 5
}

Fixing the Focus

There are two problems to solve. The first is that the entry field isn’t selected when the app starts. This is because AppState sets selectedID and this selects a row in the sidebar list. That gives the row focus and not the text field. You’ll fix this with another modifier for the field.

// 1
.onAppear {
  // 2
  entryFieldHasFocus = true
}
Setting focus on start.
Cicgelh yeyey ag rqipn.

.onChange(of: game.gameStatus) {
.onChange(of: game.id) {

Observing Properties

Earlier in this chapter, you created a @State object and used the environment modifier to pass it around. But there’s another way to use this @State object to send data to the subviews.

ContentView(appState: appState)
let appState: AppState
ContentView(appState: AppState())
@Bindable var appState: AppState
List(appState.games, selection: $appState.selectedID) { game in
SidebarView(appState: AppState())
@Bindable var appState: AppState
GuessesView(game: $appState.games[appState.gameIndex])
GameView(appState: AppState())
Observed objects build errors
Oqhodxax atsovns seexd urqaff

NavigationSplitView {
  SidebarView(appState: appState)
} detail: {
  GameView(appState: appState)
}

Key Points

  • @Observable is a protocol for classes that publishes changes to their properties.
  • The view that owns the Observable declares it using @State.
  • Subviews can access this object using @Environment, let, var or @Bindable.
  • Lists can display a selectable array of SwiftUI views.
  • Understanding data flow is crucial to working in SwiftUI.

Where to Go From Here

You’ve learned a common SwiftUI pattern with a structure for individual data elements and a class to collect them together and pass them round the app.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now