Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.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

11. Managing Data With Property Wrappers
Written by Audrey Tam

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In your SwiftUI app, every data value or object that can change needs a single source of truth and a mechanism to enable views to change or observe it. SwiftUI’s property wrappers enable you to declare how each view interacts with mutable data.

In this chapter, you’ll review how you managed data values and objects in HIITFit with @State, @Binding, @Environment, ObservableObject, @StateObject and @EnvironmentObject. And, you’ll build a simple app that lets you focus on how to use these property wrappers. You’ll also learn about TextField, the environment modifier and property wrappers @ObservedObject and @FocusState.

To help answer the question “struct or class?”, you’ll see why HistoryStore should be a class, not a structure and learn about the natural architecture for SwiftUI apps: the Model-View pattern.

Getting Started

➤ Open the TIL project in the starter folder. The project name “TIL” is the acronym for “Today I Learned”. Or, you can think of it as “Things I Learned”. Here’s how the app should work: The user taps the + button to add acronyms like “YOLO” and “BTW”, and the main screen displays these.

TIL in action
TIL in action

This app embeds a VStack in a NavigationStack, which gives you the navigation bar where you display the title and a toolbar where you display the + button. You’ll learn more about NavigationStack in Section 3.

This project has a ThingStore, which is like HistoryStore in HIITFit. This app is much simpler than HIITFit, so you can focus on how you manage the data.

Remember how you managed changes to HistoryStore in HIITFit:

HIITFit: HistoryStore shared as EnvironmentObject
HIITFit: HistoryStore shared as EnvironmentObject

In Chapter 6, “Observing Objects”, you converted HistoryStore from a structure to a class conforming to ObservableObject and set it up as an @EnvironmentObject so ExerciseView and HistoryView could access it directly.

HistoryView is a subview of WelcomeView, but you saw how using @EnvironmentObject allowed you to avoid passing HistoryStore to WelcomeView, which doesn’t use it.

If you did the challenge in that chapter, you also managed HistoryStore with @State and @Binding.

In Chapter 8, “Saving History Data”, you moved the initialization of HistoryStore from ContentView to HIITFitApp to initialize it with or without saved history data.

ThingStore has the property things, which is an array of String values. Like the HistoryStore in the first version of HIITFit, it’s a structure.

In this chapter, you’ll first manage changes to the ThingStore structure using @State and @Binding, then convert it to an ObservableObject class and manage changes with @StateObject and @ObservedObject:

TIL: ThingStore shared as Binding and as ObservedObject
TIL: ThingStore shared as Binding and as ObservedObject

You’ll learn that these two approaches are very similar.

Note: Our tutorial Property Wrappers extends this project to use ThingStore as an @EnvironmentObject.

Tools for Managing Data

You already know that a @State property is a source of truth. A view that owns a @State property can pass either its value or its binding to its subviews. If it passes a binding to a subview, that subview now has a reference to the source of truth. This allows the subview to update that property’s value or redraw itself when that value changes. When a @State value changes, any view with a reference to it invalidates its appearance and redraws itself to display the new state.

Managing UI values and model objects
Vedolamz EA toxuuj occ cafuj ogfoxxl

Property Wrappers

Property wrappers encapsulate a value or object in a structure with two properties:

Saving/Persisting App or Scene State

There are two other property wrappers you’ve used. @AppStorage wraps UserDefault values. In Chapter 7, “Saving Settings”, you used @AppStorage to save exercise ratings in UserDefaults and load them when the app launches. In the same chapter, you used @SceneStorage to save and restore the state of scenes — windows in the iPad simulator, each showing a different exercise.

Managing UI State Values

@State and @Binding value properties are mainly used to manage the state of your app’s user interface.

Managing ThingStore With @State & @Binding

TIL is a very simple app, making it easy to examine different ways to manage the app’s data. First, you’ll manage ThingStore the same way as any other mutable value you share between your app’s views.

Starter TIL
Bwipyad SEW

@State private var myThings = ThingStore()
let tempThings = ["YOLO", "BTW"]  // delete this line
ForEach(myThings.things, id: \.self) { thing in

When There Are No Things

Nothing to see here
Nothing to see here

if myThings.things.isEmpty {
  Text("Add acronyms you learn")
    .foregroundColor(.gray)
}
First-time empty-array screen
Qomkp-pomi orjst-ijyov mxmeom

@Binding var someThings: ThingStore
someThings.things.append("FOMO")
AddThingView(someThings: .constant(ThingStore()))
AddThingView(someThings: $myThings)
Adding a string works.
Agrazt i mpvajb pesxf.

Using a TextField

Many UI controls work by binding a parameter to a @State property of the view: These include Slider, Toggle, Picker and TextField.

@State private var thing = ""
TextField("Thing I Learned", text: $thing)  // 1
  .textFieldStyle(.roundedBorder)  // 2
  .padding()  // 3
if !thing.isEmpty {
  someThings.things.append(thing)
}
TextField input
GuftToacy uttuh

Improving the UX

You can improve your users’ experience by adjusting how the text field handles their input. Acronyms should appear as all caps, but it’s easy to forget to hold down the Shift key. Sometimes the app auto-corrects your acronym: FTW to FEW or FOMO to DINO. And focus! Your users will really appreciate having the cursor already in the text field, so they don’t have to tap there first.

.autocapitalization(.allCharacters)
.disableAutocorrection(true)
@FocusState private var thingIsFocused: Bool
.focused($thingIsFocused)
.onAppear { thingIsFocused = true }
Auto-focus, auto-cap
Ueta-tabah, oowa-par

Accessing Environment Values

A view can access many environment values like accessibilityEnabled, colorScheme, lineSpacing, font and dismiss. Apple’s SwiftUI documentation has a full list of environment values.

Text("Add acronyms you learn")
Text view attributes: Many are inherited.
Coqm noim uylvesulek: Cewx ite oqxigezir.

HStack {
  Image(systemName: "1.circle")
    .font(.largeTitle)
  Image(systemName: "2.circle")
  Image(systemName: "3.circle")
  Image(systemName: "4.circle")
}
.font(.title2)

Modifying Environment Values

AddThingView already uses the dismiss environment value, declared as a view property the same as in HIITFit’s SuccessView. But, you can also set environment values by modifying a view.

.environment(\.textCase, .uppercase)
Automagic uppercase
Oojozemos eshuqruxa

.environment(\.textCase, nil)
No upper case conversion in AddThing
Yo urxug kede nevzenwuiq is EzkTtitx

Managing Model Data Objects

@State, @Binding and @Environment only work with value data types. Simple built-in data types like Int, Bool or String are useful for defining the state of your app’s user interface.

Class & Structure

Actually, ThingStore should be a class, not a structure. @State and @Binding work well enough to update the ThingStore source of truth value in ContentView from AddThingView. But ThingStore isn’t the most natural use of a structure. For the way your app uses ThingStore, a class is a better fit.

Managing ThingStore With @StateObject & @ObservedObject

You’ve already used @EnvironmentObject in Chapter 6, “Observing Objects”, to avoid passing HistoryStore through WelcomeView to reach HistoryView.

final class ThingStore: ObservableObject {
  @Published var things: [String] = []
}
@EnvironmentObject var history: HistoryStore
TabView(selection: $selectedTab) {
...
}
.environmentObject(HistoryStore())
@StateObject private var myThings = ThingStore()
AddThingView(someThings: myThings)
@ObservedObject var someThings: ThingStore
AddThingView(someThings: ThingStore())
TIL in action
NOZ ay uqyoox

SwiftUI App Design Pattern

Model-View-Controller
Model-View-Controller

Model-View
Pipin-Cuij

MV in HIITFit
YT uy JIITJir

MV in TIL
TL ul PEN

Wrapping Up Property Wrappers

Here’s a summary to help you wrap your head around property wrappers.

Property wrappers for values and objects
Xkatulzw hbeysezl fef tusoac ols ezlaggh

Wrapping Values

@State and @Binding are the workhorses of value property wrappers. A view owns the value if it doesn’t receive it from any parent views. In this case, it’s a @State property — the single source of truth. When a view is first created, it initializes its @State properties. When a @State value changes, the view redraws itself, resetting everything except its @State properties.

Wrapping Objects

When your app needs to change and respond to changes in a reference type, you create a class that conforms to ObservableObject and publishes the appropriate properties. In this case, you use @StateObject and @ObservedObject in much the same way as @State and @Binding for values. You instantiate your publisher class in a view as a @StateObject then pass it to subviews as an @ObservedObject. When the owning view redraws itself, it doesn’t reset its @StateObject properties.

Key Points

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now