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:
struct ContentView: View {
var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Add")
}
}
}
}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:
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Add")
}
}
}
}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:
struct ContentView: View {
@State private var name: String = "Vin"
var body: some View {
VStack {
Text("Hello \(name)")
TextField("Change Me", text: name)
}
}
}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):
TextField("Change Me", text: name) // Incorrect
TextField("Change Me", text: $name) // CorrectReference 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:
class User {
var firstName: String = "Vin"
var lastName: String = "Bui"
}
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("Hello \(user.firstName) \(user.lastName)")
TextField("First Name", text: $user.firstName)
TextField("Last Name", text: $user.lastName)
}
}
}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
ObservableObjectprotocol.Mark the properties we want to observe with
@Published.Use
@StateObjecton our reference type property.
class User: ObservableObject {
@Published var firstName: String = "Vin"
@Published var lastName: String = "Bui"
}
struct ContentView: View {
@StateObject private var user = User()
var body: some View {
VStack {
Text("Hello \(user.firstName) \(user.lastName)")
TextField("First Name", text: $user.firstName)
TextField("Last Name", text: $user.lastName)
}
}
}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
privatefor best practice. No external source should modify@Stateproperties.Use
$for a two-way binding to the property (read and write).
@StateObject
Similar to
@Statebut used on anObservableObject(reference types).Changes to
@Publishedproperties are notified.The view itself creates and owns the instance.
@Binding
@StateCreates →@BindingReceives.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
@StateObjectCreates →@ObservedObjectReceives.Similar to
@Bindingbut used on anObservableObject(reference types).Changes to
@Publishedproperties are notified.The view itself DOES NOT create or own the instance.
@EnvironmentObject
Similar to
@ObservedObjectbut 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:
@Observable
class User {
var firstName: String = "Vin"
var lastName: String = "Bui"
}
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("Hello \(user.firstName) \(user.lastName)")
TextField("First Name", text: $user.firstName)
TextField("Last Name", text: $user.lastName)
}
}
}Last updated
Was this helpful?