Property Wrappers, introduced in Swift 5.1, simplify the code you write for SwiftUI views. For example, if you need to declare a property in your view and have the view update automatically when that property changes, you use the @State property wrapper. Property Wrappers have logic abstracted away that can accomplish various things. Choosing the right Property Wrapper for your use case will help you simplify your code.
In this tutorial, we’ll look at the @State and @StateObject property wrappers. Let’s begin with @State.
@State Property Wrapper
You declare @State in a view. The view that creates the @State variable owns the data, making the view the source of truth. The view handles all changes to the variable either directly within the view, or via a reference or binding to it. Declaring it as a single source of truth means that there is one piece of correct data.
The benefit of this is that with their being one source of truth, any child view that modifies the state, or indeed, even if the view that owns the data modifies it, the changes will be reflected in all those places, keeping a consistent state.
Let’s take a look at an example of how to use the @State property wrapper.
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
Button("Count: \(count)") {
count += 1
}
}
}
In this example, we declare the count variable as an integer initialised at zero. It is set as private and uses the @State property wrapper.
It is essential to give it an initial value here because SwiftUI views require data to be initialised in the view before it is displayed.
Next, we declare the body and add a button that displays the count in the title and increments it by one each time it is pressed.
Because we used @State, this property wrapper updates the view each time the button is pressed. All of that logic is kept behind the scenes by being abstracted away.
Let’s now take a look at using a binding and passing it to a child view to modify the value.
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
.font(.largeTitle)
.padding()
Button("ContentView Button: \(count)") {
count += 1
}
ButtonView(count: $count)
}
}
}
struct ButtonView: View {
@Binding var count: Int
var body: some View {
Button("ButtonView Button: \(count)") {
count += 1
}
}
}
The example above has some changes in that a child view, called ButtonView, has been introduced. We also copied the button into that view and called this view from the ContentView. A title has also been added that displays the count on the parent view.
In ButtonView, you will notice that a count that uses the @Binding property wrapper is declared. The ContentView calls the ButtonView and passes the count with what is called binding syntax (see the $ sign before the variable name).
When previewing this in Xcode, you will see a title and two buttons under it. If you tap on any of the two buttons, all places that specify the count are updated. What is happening here is that the ContentView is the source of truth. When the button is pressed, the @State count variable is updated which triggers the view to refresh. Because we pass the count to the child view, that count also gets updated.
Likewise, when you tap the button in the child view, because of the binding the source of truth is updated in the ContentView which triggers the refresh, and all places that reference the count variable are updated.
By using this, you can ensure that all views using this @State variable are all updated to keep the data consistent.
@State is typically used for value types such as booleans, strings, integers, and so on. However, if you have a class that doesn’t declare published properties and uses the @Observable macro, then you can also instantiate one of these and pass it around. It is declared as follows:
@State private var user = User()
When declaring a class with the @Observable macro, it will instantiates its default value. The documentation suggests avoiding intensive work.
Overall, @State is an excellent and commonly used property wrapper.
@StateObject Property Wrapper
If you need to store a reference type rather than a value type, then the state object property wrapper can be used. The @StateObject property wrapper is a single source of truth for a reference type. You would store this at the top of the view hierarchy where you need to use it.
To be compatible with @StateObject, a class needs to conform to ObservableObject as follows:
class UserModel: ObservableObject {
@Published var firstName: String = "John"
@Published var lastName: String = "Smith"
}
struct ContentView: View {
@StateObject private var user = UserModel()
var body: some View {
VStack {
HStack {
Text(user.firstName)
Text(user.lastName)
}
}
}
}
In this code we have a UserModel class that conforms to ObservableObject. It has two published properties.
In the ContentView structure we have the user declared with the @StateObject property wrapper. It is declared as private to avoid it being set by a memberwise initialiser.
We can access the values with user.firstName, and so on.
Passing the user to a child view is done as follows:
class UserModel: ObservableObject {
@Published var firstName: String = "John"
@Published var lastName: String = "Smith"
}
struct ContentView: View {
@StateObject private var user = UserModel()
var body: some View {
VStack {
HStack {
Text(user.firstName)
Text(user.lastName)
}
NameView()
.environmentObject(user)
}
}
}
struct NameView: View {
@EnvironmentObject var user: UserModel
var body: some View {
HStack {
TextField("First Name:", text: $user.firstName)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Last Name:", text: $user.lastName)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
A few adjustments can be found in this next example of code where we have now added a NameView struct that has two TextFields in a HStack. At first, it declares an @EnvironmentObject called user that is a UserModel.
We access this with the $user.firstName syntax. Any changes here are reflected in the source of truth in the view above.
When we call NameView from the ContentView, we add in the .environmentObject and pass in the user.
When any of the properties are changed anywhere in the code, either in the parent or a child, just like @State, the views that are associated with the @StateObject will all be updated.
I hope this shows you a couple of simple examples of how @State and @StateObject can be used. Property wrappers in these instances can save a lot of time, and a lot of boiler plate code with it being abstracted away from the user.
Leave a Reply
You must be logged in to post a comment.