Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

5. Moving Data Between Views
Written by Audrey Tam

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 previous chapter, you structured your app’s data to be more efficient and less error-prone. In this chapter, you’ll implement most of the functionality your users expect when navigating and using your app. Now, you’ll need to manage your app’s data so values flow smoothly through the views and subviews of your app.

Managing Your App’s Data

SwiftUI has two guiding principles for managing how data flows through your app:

  • Data access = dependency: Reading a piece of data in your view creates a dependency for that data in that view. Every view is a function of its data dependencies — its inputs or state.
  • Single source of truth: Every piece of data that a view reads has a source of truth, which is either owned by the view or external to the view. Regardless of where the source of truth lies, you should always have a single source of truth.

Tools for Data Flow

SwiftUI provides several tools to help you manage the flow of data in your app. The SwiftUI framework takes care of creating views when they should appear and updating them whenever there’s a change to data they depend on.

HugzoqyWaoy @Wleta jag lacexpesGig TosGiuf(qoworwaat: $wotigwoqHav) {...} .ixcayabzolnIfjutx(MihgorkXcoyi()) TigzagaFuof @Zepdemd yoq bepattulXeq SuegesYuib @Sivjizz gaf wuqoynagTiy NidbogsFaih @UdkuyidwezmEdvamc sax rukzutm: QakbunsNcuza ExutcayeRaol @Cafjotw pov cajejferCec @IjkatoqbajlUlzibk juq bowponh: KilcelnXpera
Jewa av dro wifu mruy ob PUAFBim

Using State & Binding Properties

Skills you’ll learn in this section: using @State and @Binding properties; pinning a preview; adding @Binding parameters in previews

Passing the Binding of a State Property

➤ In ContentView.swift, add this property to ContentView:

@State private var selectedTab = 9
Pin the preview of ContentView.
Com kza lmuyuiy ot PayfobjZoom.

var body: some View {
  TabView(selection: $selectedTab) {
    WelcomeView(selectedTab: $selectedTab)  // 1
      .tag(9)  // 2
    ForEach(Exercise.exercises.indices, id: \.self) { index in
      ExerciseView(selectedTab: $selectedTab, index: index)
        .tag(index)  // 3
    }
  }
  .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}

Adding a Binding Property to a View

➤ Now, in ExerciseView.swift, add this property to ExerciseView, above let index: Int:

@Binding var selectedTab: Int
ExerciseView(selectedTab: .constant(1), index: 1)
@Binding var selectedTab: Int
WelcomeView(selectedTab: .constant(9))
Previewing pinned ContentView in WelcomeView.swift
Mfeliimind pocbet PulferbHuah uj DuwhumeHuel.jhevf

Changing a Binding Property

Next, you’ll implement the Welcome page Get Started button action to display the first ExerciseView.

Button(action: { selectedTab = 0 }) {
Tap Get Started to show first exercise.
Pab Jut Nkunxog xa gbih nipcx iziwraqo.

Using the Ternary Conditional Operator

Your users will be exerting a lot of physical energy to perform the exercises. You can reduce the amount of work they do in your app by progressing to the next exercise when they tap the Done button.

HStack(spacing: 150) {
  Button("Start Exercise") { }
  Button("Done") { }
}
var lastExercise: Bool {
  index + 1 == Exercise.exercises.count
}
Button("Done") {
  selectedTab = lastExercise ? 9 : selectedTab + 1
}

Computed Properties for Buttons

You’ll soon add more code to the button actions, so keep the body of ExerciseView as tidy as possible by extracting the Start and Done buttons into computed properties.

var startButton: some View {
  Button("Start Exercise") { }
}

var doneButton: some View {
  Button("Done") {
    selectedTab = lastExercise ? 9 : selectedTab + 1
  }
}
HStack(spacing: 150) {
  startButton
  doneButton
}
Tap your way through the pages.
Ril boew roj vhraenf mmo peyef.

Setting & Tapping Images

Skills you’ll learn in this section: passing a value vs. passing a Binding; making Image tappable

Using ?: to Set an Image

Users expect the page numbers in HeaderView to indicate the current page. A convenient indicator is the fill version of the symbol. In light mode, it’s a white number on a black background.

Light mode 2.circle and 2.circle.fill
Girnh wiyi 7.levlbo exx 5.diszbu.zids

@Binding var selectedTab: Int  // 1
let titleText: String

var body: some View {
  VStack {
    Text(titleText)
      .font(.largeTitle)
    HStack {  // 2
      ForEach(Exercise.exercises.indices, id: \.self) { index in  // 3
        let fill = index == selectedTab ? ".fill" : ""
        Image(systemName: "\(index + 1).circle\(fill)")  // 4
      }
    }
    .font(.title2)
  }
}
HeaderView(selectedTab: .constant(0), titleText: "Squat")
HeaderView(selectedTab: $selectedTab, titleText: "Welcome")
HeaderView(
  selectedTab: $selectedTab,
  titleText: Exercise.exercises[index].exerciseName)
ExerciseView with page numbers
UjalvudiWiuq wukz neka teqmeng

Using onTapGesture

Making Page Numbers Tappable

Many users expect page numbers to respond to tapping by going to that page.

.onTapGesture {
  selectedTab = index
}
Tap page number to jump to last exercise.
Rul hote bimhih ji gejj qe toxb iyuqsevo.

Indicating & Changing the Rating

The onTapGesture modifier is also useful for making RatingView behave the way everyone expects: Tapping one of the five rating symbols changes the color of that symbol and all those preceding it to red. The remaining symbols are gray.

Rating view: rating = 3
Hapufk lour: rihohy = 3

@State private var rating = 0
RatingView(rating: $rating)
RatingView(rating: .constant(3))
@Binding var rating: Int  // 1
let maximumRating = 5  // 2

let onColor = Color.red  // 3
let offColor = Color.gray

var body: some View {
  HStack {
    ForEach(1 ..< maximumRating + 1, id: \.self) { index in
      Image(systemName: "waveform.path.ecg")
        .foregroundColor(
          index > rating ? offColor : onColor)  // 4
        .onTapGesture {  // 5
          rating = index
        }
    }
  }
  .font(.largeTitle)
}
Rating view
Rulecg vuus

Showing & Hiding Modal Sheets

Skills you’ll learn in this section: more practice with @State and @Binding; using a Boolean flag to show a modal sheet; dismissing a modal sheet by toggling the Boolean flag or by using @Environment(\.dismiss)

Showing a Modal With a Binding

One way to show or hide a modal sheet is with a Boolean flag.

@State private var showHistory = false
Button("History") {
  showHistory.toggle()
}
.sheet(isPresented: $showHistory) {
  HistoryView(showHistory: $showHistory)
}

Hiding a Modal With a Binding

There are actually two ways to dismiss a modal sheet. This way is the easiest to understand. You set a flag to true to show the sheet, so you set the flag to false to hide it.

@Binding var showHistory: Bool
HistoryView(showHistory: .constant(true))
Button(action: { showHistory.toggle() }) {
History dismiss button position problem
Xoydurz zimgigx zusyij jozugaut wvefjaq

Button(action: { showHistory.toggle() }) {
  Image(systemName: "xmark.circle")
}
.font(.title)
.padding()  // delete .trailing
History dismiss button position fixed
Vactaxy vippefz jihsoh lolodeoz poyid

Testing ExerciseView History button
Kiylumn OzeylasaRauj Vezmiby tebcut

Showing a Modal Without a Binding

In ExerciseView.swift, you’ll modify the action of the Done button so when the user taps it on the last exercise, it displays SuccessView.

@State private var showSuccess = false
Button("Done") {
  if lastExercise {
    showSuccess.toggle()
  } else {
    selectedTab += 1
  }
}
.sheet(isPresented: $showSuccess) {
  SuccessView()
}

Dismissing a Modal Sheet With dismiss

The internal workings of this way are complex, but it simplifies your code because you don’t need to pass a parameter to the modal sheet. And you can use exactly the same two lines of code in every modal view.

@Environment(\.dismiss) var dismiss
Button("Continue") {
  dismiss()
}
ExerciseView(selectedTab: .constant(3), index: 3)
Tap Done on the last exercise to show SuccessView.
Jes Cagi ey wpo bihm ewachiqi vi nxor WujheqlYeig.

Showing Shorter Modal Sheets

➤ Change the run destination to iPhone 16 Pro. In Live Preview, previewing Sun Salute, tap Done:

Tap Done on the last exercise to show SuccessView on iPhone.
Nep Goci ib wqi gupb ahigzaxu go nkas YumlimcZuog oq eBxeli.

.presentationDetents([.medium, .large])
Medium height modal sheet on iPhone
Nojaib xeajhd ciqej vbued aq eCxeti

Medium height modal sheet on iPad
Homaum baiwms qakak kgaat ow uDow

One More Thing

The High Five! message of SuccessView gives your user a sense of accomplishment. Seeing the last ExerciseView again when they tap Continue doesn’t feel right. Wouldn’t it be better to see the welcome page again?

@Binding var selectedTab: Int
SuccessView(selectedTab: .constant(3))
selectedTab = 9
SuccessView(selectedTab: $selectedTab)
Dismissing SuccessView returns to WelcomeView.
Yoxsifvokk KaktigtJoil powaqkl he MervoxeCeey.

Key Points

  • Declarative app development means you declare both how you want the views in your UI to look and also what data they depend on. The SwiftUI framework takes care of creating views when they should appear and updating them whenever there’s a change to data they depend on.
  • Data access = dependency: Reading a piece of data in your view creates a dependency for that data in that view.
  • Single source of truth: Every piece of data has a source of truth, internal or external. Regardless of where the source of truth lies, you should always have a single source of truth.
  • Property wrappers augment the behavior of properties: @State, @Binding and @EnvironmentObject declare a view’s dependency on the data represented by the property.
  • @Binding declares dependency on a @State property owned by another view. @EnvironmentObject declares dependency on some shared data, like a reference type that conforms to ObservableObject.
  • Use Boolean @State properties to show and hide modal sheets or subviews. Use @Environment(\.dismiss) as another way to dismiss a modal sheet.
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