SwiftUI Property Wrappers
Learn different ways to use SwiftUI property wrappers to manage changes to an app’s data values and objects. By Audrey Tam.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftUI Property Wrappers
35 mins
- Getting Started
- Tools for Managing Data
- Property Wrappers
- Managing UI State Values
- Managing ThingStore With @State and @Binding
- Using a TextField
- Accessing Environment Values
- Modifying Environment Values
- Managing Model Data Objects
- Class and Structure
- Managing ThingStore With @StateObject and @ObservedObject
- Refactoring TIL
- Using Thing Structure
- Navigating to ThingView
- Adding a New Thing From ThingView
- Using @EnvironmentObject
- Wrapping Up Property Wrappers
- Wrapping Values
- Wrapping Objects
- One More Thing
- Where to Go From Here?
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
.
To get user input via a TextField
, you need a mutable String
property to store the user’s input.
In AddThingView.swift, add this property to AddThingView
:
@State private var thing = ""
It’s a @State
property because it must persist when the view redraws itself. AddThingView
owns this property, so it’s responsible for initializing thing
. You initialize it to the empty string.
Now, add your TextField
in the VStack
, above the Done button:
TextField("Thing I Learned", text: $thing) // 1
.textFieldStyle(RoundedBorderTextFieldStyle()) // 2
.padding() // 3
- The label “Thing I Learned” is the placeholder text. It appears grayed out in the
TextField
as a hint to the user. You pass a binding tothing
soTextField
can set this value to what the user types. - You dress up this
TextField
with a rounded border. - You add padding so there’s some space from the top of the view and also to the button.
Then, edit what the button action appends:
if !thing.isEmpty {
someThings.things.append(thing)
}
Instead of "FOMO"
, you append the user’s text input to your things
array after checking it’s not the empty string.
Refresh live-preview in the ContentView
preview and tap +. Type an acronym like YOLO in the text field. It automatically capitalizes the first letter, but you must hold down the Shift key for the rest of the letters. Tap Done:
ContentView
displays your new acronym.
Sometimes the text field auto-corrects your acronym: FTW to GET or FOMO to DINO.
Add this modifier to TextField
:
.disableAutocorrection(true)
Accessing Environment Values
A view can access many environment values like accessibilityEnabled
, colorScheme
, lineSpacing
, font
and presentationMode
. Apple’s SwiftUI documentation has a full list of environment values.
A view’s environment is a kind of inheritance mechanism. A view inherits environment values from its ancestor views, and its subviews inherit its environment values.
To see this, open ContentView.swift and click anywhere in this line:
Text("Add acronyms you learn")
Now open the Attributes inspector:
Font, Weight, Line Limit, Padding and Frame Size are Inherited. Font Color would also be inherited if you hadn’t set it to Gray.
A view can override an inherited environment value. It’s common to set a default font for a stack then override it for the text in a subview of the stack.
Modifying Environment Values
AddThingView
already uses the presentationMode
environment value, declared as a view property. But, you can also set environment values by modifying a view.
Acronyms should appear as all caps but it’s easy to forget to hold down the Shift key. You can actually set an environment value to automatically convert text to upper case.
In TILApp.swift, add this modifier to ContentView()
:
.environment(\.textCase, .uppercase)
You set uppercase
as the default value of textCase
for ContentView
and all its subviews.
.textCase(.uppercase)
also works, but the .environment
syntax highlights the fact that textCase
is an environment value.
To see it in live preview, also add this modifier in ContentView.swift to ContentView()
in previews
.
Refresh live-preview, add acronyms without bothering to keep all the letters uppercase. Just type yolo or fomo. Tap DONE. Notice this label and the placeholder text are now all uppercase:
Your strings are automatically converted to upper case.
The environment value applies to all text in your app, which looks a little strange. No problem — you can override it.
In AddThingView
, add this modifier to the VStack
:
.textCase(nil)
You set the value to nil
, so none of the text displayed by this VStack
is converted to uppercase.
Refresh live-preview, tap +, type icymi then tap Done:
Now, the button label and placeholder text are back to normal. The uppercase
environment default still converts your strings to all caps on the main screen.
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.
You can use custom value data types like struct
or enum
to model your app’s data. And, you can use @State
and @Binding
to manage updates to these values, as you did earlier in this tutorial.
Most apps also use classes to model data. SwiftUI provides a different mechanism to manage changes to class objects: ObservableObject
, @StateObject
, @ObservedObject
and @EnvironmentObject
. To practice using @ObservedObject
, you’ll refactor TIL to use @StateObject
and @ObservedObject
to update ThingStore
, which conforms to ObservableObject
. You’ll see a lot of similarities, and a few differences, to using @State
and @Binding
.
Class and Structure
But, this section isn’t just to practice managing objects. ThingStore
actually 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.
A class is more suitable when you need shared mutable state like ThingStore
. A structure is more suitable when you need multiple independent states like the Thing
structures you’ll create later in this tutorial.
For a class object, change is normal. A class object expects its properties to change. For a structure instance, change is exceptional. A structure instance requires advance notice that a method might change a property.
A class object expects to be shared, and any reference can be used to change its properties. A structure instance lets itself be copied, but its copies change independently of it and of each other.
Managing ThingStore With @StateObject and @ObservedObject
To use ThingStore
as an @ObservedObject
, you’ll convert it from a structure to a class that conforms to ObservableObject
. Then, you’ll create it as a @StateObject
and pass it to a subview that uses it as an @ObservedObject
. Sounds a lot like “create a @State
property and pass its @Binding
“, doesn’t it?
@State
value or a @StateObject
to a subview as a @Binding
or @ObservedObject
property, even if that subview needs only read access. This enables the subview to redraw itself whenever the @State
value or ObservableObject
changes.
In ContentView.swift, replace the ThingStore
structure with the following:
final class ThingStore: ObservableObject {
@Published var things: [String] = []
}
You make ThingStore
a class instead of a structure, then make it conform to ObservableObject
. You mark this class final
to tell the compiler it doesn’t have to check for any subclasses overriding properties or methods.
ThingStore
publishes its array of data. A view subscribes to this publisher by declaring it as a @StateObject
, @ObservedObject
or @EnvironmentObject
. Any change to things
notifies subscriber views to redraw themselves.
In TIL, AddThingView
will use an @ObservedObject
, so you must instantiate the model object as a @StateObject
in an ancestor view, then pass it as a parameter to its subviews. The owning view creates the @StateObject
exactly once.
In ContentView
, replace @State private var myThings = ThingStore()
with this line:
@StateObject private var myThings = ThingStore()
ThingStore
is now a class, not a structure, so you can’t use the @State
property wrapper. Instead, you use @StateObject
.
@State
property, but its “value” is its address in memory, so dependent views will redraw themselves only when its address changes — for example, when the app reinitializes it.
The @StateObject
property wrapper ensures myThings
is instantiated only once. It persists when ContentView
redraws itself.
In the call to AddThingView(someThings:)
, remove the binding symbol $
:
AddThingView(someThings: myThings)
You don’t need to create a reference to myThings
. As a class object, it’s already a reference.
In AddThingView.swift, replace @Binding
in AddThingView
with @ObservedObject
:
@ObservedObject var someThings: ThingStore
ThingStore
had more properties and you wanted to restrict write access to its things
array, you could pass $myThings.things
to AddThingView
, which would have a @Binding someThings: [String]
property.
And fix its previews
:
AddThingView(someThings: ThingStore())
The argument isn’t a binding anymore.
Refresh live-preview, tap +, type yolo then tap Done:
No surprise: The app still works the same as before.