6️⃣Property Wrappers
Fall 2023 | Vin Bui
Because SwiftUI is declarative, the way we write the logic in our apps is completely different from how we would do it in UIKit. Let’s take a look at the following code that increments the value inside of a Text
by 1 every time we click on a button:
The above code will give us an error for the line containing count += 1
indicating that ‘self’ is immutable. Why? That’s because we are using a struct. When we change the value of a property inside of a struct, the entire struct changes. If that’s the case, then how do we keep the struct object alive while being able to change the value of its properties? We use property wrappers.
Using @State
To make the above code work, we can add the @State
property wrapper to the count
property. Because this object creates and owns this property, we should mark it as private
for good practice:
The powerful thing about using these property wrappers is that SwiftUI automatically updates the views for us. When we incremented the value of count
, the Text
view using this property gets updated automatically.
Two Way Binding
Now, let’s change things up and use a String with a TextField
:
If we try to run the above code, it will not work. That’s because the name
variable that we pass into the TextField
is a String
and NOT Binding<String>
. What does this “binding” mean? Let’s think about the difference between a Text
and a TextField
. A Text
only reads the value that we pass in whereas a TextField
reads AND writes a value. In this case, we must add a $
to perform a two-way binding (in other words, a read and a write):
Reference Types
You may have noticed that we only used value types in the examples above. That’s because @State
only works for value types such as structs, strings, ints, etc. However, there are times when we want to pass one reference around to multiple views. In this case, we want to use a reference type.
Consider the following code:
If we change the values inside of the text fields, the Text
view will not be updated properly. One way to fix this is to change the User
class to a struct. However, we want to keep it as a reference type.
When dealing with reference types, we must use a different property wrapper: @StateObject
. To properly use this property wrapper, we must do the following (iOS 16 and below):
Conform our class to the
ObservableObject
protocol.Mark the properties we want to observe with
@Published
.Use
@StateObject
on our reference type property.
You may be wondering, “Why does @State
work for structs but not classes?” When we use @State
SwiftUI observes the entire value. When we change the property of a struct, the entire value changes since structs are value types. On the other hand, when we change the property of a class, the entire object does not change and a new copy is not created like it does with structs.
Property Wrappers Cheat Sheet
The above property wrappers that we used only works if the view creates and owns the property. However, properties are often shared among views. If one child view changes a property, we also want to update it in the parent view. There are also different ways of sharing properties: (1) either directly (1-on-1) from a parent to child or (2) shared among every view through the environment.
Below is a list of some of the most common property wrappers and when we would use them.
@State
The view itself creates and owns the instance we want to wrap.
Property being wrapped is a value type (such as struct or enum).
Mark as
private
for best practice. No external source should modify@State
properties.Use
$
for a two-way binding to the property (read and write).
@StateObject
Similar to
@State
but used on anObservableObject
(reference types).Changes to
@Published
properties are notified.The view itself creates and owns the instance.
@Binding
@State
Creates →@Binding
Receives.Read and write to a property that's owned by a parent view.
Property being wrapped is a value type (such as struct or enum).
Use
$
for a two-way binding to the property (read and write).
@ObservedObject
@StateObject
Creates →@ObservedObject
Receives.Similar to
@Binding
but used on anObservableObject
(reference types).Changes to
@Published
properties are notified.The view itself DOES NOT create or own the instance.
@EnvironmentObject
Similar to
@ObservedObject
but shared to MULTIPLE views.The view itself DOES NOT create or own the instance.
Flow Chart
Where does the data come from?
Owned
Immutable Value? Regular Property
Mutable Value? @State
ObservableObject? @StateObject
Parent
Immutable Value? Regular Property
Mutable Value? @Binding
ObservableObject? @ObservedObject
Environment (@Environment is to @EnvironmentObject → @State is to @StateObject)
EnvironmentalValues, keyPath? @Environment
ObservableObject? @EnvironmentObject
Saved on Disk
UserDefaults?
Whole app? @AppStorage
Single scene (for MacOS)? @SceneStorage
CoreData? @FetchRequest
iOS 17 Onwards
Starting with iOS 17, we longer need to mark our class properties with @Published
and conform our class to ObservableObject
. All we have to do is add the @Observable
property wrapper to our class. Additionally, we can just simply use @State
on the class object.
Below is an iOS 17 version of the code we used earlier:
Last updated