The @State Property Wrapper in SwiftUI Explained
With @State
, you tell SwiftUI that a view is now dependent on some state. If the state changes, so should the User Interface. It’s a core principle of SwiftUI: data drives the UI. But how does that work?
In this tutorial, we’ll discuss how you use @State
with SwiftUI. We’ll create a simple view that updates some text as you type in a text field. And of course, we’ll deconstruct what’s going on behind the scenes, too.
Here’s what we’ll get into:
- How does
@State
work in SwiftUI, and what’s it for? - Principles like data drives UI and single source of truth
- What are property wrappers and how do they work?
- How to connect a
TextField
to@State
with a binding - Your new pals
$name
and_name
Ready? Let’s go.
- What’s a Property Wrapper?
- Using @State in SwiftUI
- How State Works
- @State and Bindings
- Further Reading
What’s a Property Wrapper?
First things first. In SwiftUI, @State
is one of many property wrappers you can use. But what’s a property wrapper?
A property wrapper “wraps” a property (o rly?) and augments it with additional behavior. In essence, a property wrapper adds a whole lot of code to a property – and that gives the property superpowers. You can do more with the property now.
You can compare property wrappers with weak or @IBOutlet. The weak
attribute changes the way that property is retained in memory; the attribute makes a property available for connecting in Interface Builder. Property wrappers are more powerful than that, though, and they integrate deeply with SwiftUI.
Author’s Note: I like to think of property wrappers as burritos. See, a burrito’s filling is just salsa, meat, beans and guacamole without the tortilla. It might as well be chili con carne without the ‘wrap’. Thanks to the tortilla, you can pick up the burrito and eat it. The tortilla wraps the filling, enabling you to do more, just like @State
wraps a simple property, making it more.
Using @State in SwiftUI
A core principle of SwiftUI is that data drives UI. Whenever some data in your app changes, the User Interface (UI) that represents (and observes) that data changes too. You never directly manipulate or change the UI; you change the data, and your app’s UI subsequently updates.
You can use the word state when you talk about data. For example, when your friend’s “state” is angry, their face changes from neutral to frowned to stark raving mad. If emotions are data, they can be represented by different states, and that changes your friend’s facial expression.
SwiftUI is based on the idea that every state results in a different but deterministic UI change. By making data the driver of the UI, SwiftUI can efficiently build and update UIs. It’s also easier to reason about your code this way, especially compared to the usual view controller mayhem.
Let’s take a look at some code:
struct ContentView: View
{
var name: String = "Bob and Alice"
var body: some View {
Text("Hello, \(name)!")
}
}
In the above SwiftUI code, we’ve defined a simple Text
view that’ll display a greeting based on a property name
of type String
. As-is, this view will always be the same because the state of the view is static. It doesn’t change.
Now, let’s say we want to change the greeting based on some text input. We change the code to this:
struct ContentView: View
{
var name: String = "Bob and Alice"
var body: some View {
VStack {
Text("Hello, \(name)!")
TextField("Name", text: ···)
}
}
}
In the above code, we’ve added a TextField
view below the Text
view. We’re still making a greeting based on the name
property. But views in SwiftUI are immutable; you can’t change their properties. How can we make the greeting dynamic, and use the text in the TextField
to change the UI?
That’s where @State
comes in. We can add this property wrapper to name
, and by doing so we tell SwiftUI that the view is now dependent on the state of the name
property.
SwiftUI will now manage the storage of this property, because we’ve declared it as state. Whenever the name
property changes, the User Interface (i.e., the view) must update too.
We’ll also bind the TextField
to the name
property, so the contents of the text field is written to name
. This binding connects the text field and the data.
Check this out:
struct ContentView: View
{
@State private var name: String = "Bob and Alice"
var body: some View {
VStack {
Text("Hello, \(name)!")
TextField("Name", text: $name)
}
}
}
In the above code, we’ve made a few important changes:
- The
@State
property wrapper is added to thename
property, and the property is made private. The view now depends on the state ofname
, and updates whenname
does. - The
TextField
‘stext
parameter gets a$name
argument, a dollar sign$
followed by the name of the property, i.e.name
. This creates a binding between the text field and thename
property.
Here’s how that looks in an app on iPhone Simulator:
Quick Note: You use @State
for “view-local” or “internal” state, which is data that doesn’t leave the view. You can use @ObservedObject, and others, for external state. Because @State
is local, it’s conventional to mark the property with private
. This emphasizes that the state does not leave the view.
In essence, 2 core principles are at play here:
- The
@State
property makes the view dependent on the state ofname
, so whenever the text value ofname
changes, the view is updated to show that change. - The
TextField
receives a binding to thename
property, which enables the text field to change the text value ofname
. More on bindings later.
Awesome! Let’s take a closer look.
How State Works
Like most things SwiftUI, working with state is really many moving parts working together in tandem. A few principles to keep in mind:
- In SwiftUI, data drives the UI
- Views are immutable; you can’t just change the
name
property -
@State
makes the view dependent on a state; on the value of a property - With
@State
, we’re telling SwiftUI to manage the property’s storage for us - When the property’s value – state – changes, the view must update too
It almost feels like magic! It’s real though, no magic. Like the Wizard of Oz, there’s a tiny man behind the scenes pulling ropes and wires. And because it’s real, we can discover and understand how it works.
When you annotate the name
property with the @State
property wrapper, SwiftUI will synthesize 2 additional properties: $name
and _name
. When your property is called cookies
, those other properties are called $cookies
and _cookies
, and so on.
You now have 3 properties:
-
name
is the (actual) wrapped value of the property, i.e."Bob and Alice"
-
_name
is the property wrapper struct itself, which for@State
is State -
$name
is the projected value, that exposes additional functionality, which for@State
is a binding
These additional properties are created for you when you add the @State
attribute to a property. It’s 3 for the price of 1! But… why?
One of the goals of SwiftUI is to make building UIs more productive. SwiftUI does this by formalizing the relationship between data (i.e., state) and UI.
Instead of letting you code everything yourself, like with Storyboards, you now get a bunch of ready-made tools like @State
. This is what frameworks are for: providing functionality, so you don’t have to code it yourself.
Property wrappers are a mechanism to add functionality to properties, like managing the storage of state. The property wrapper “adds code” to the property. This is automatically synthesized; SwiftUI creates it for you, so you don’t have to.
What code does it add? The State
struct, and $name
and _name
. You still get access to the value of name
, and you also get all that other stuff. This “wrapping” (of the burrito filling!) enables you to do more with a property, like responding to state changes.
Consider that _name.wrappedValue
is equal to name
, and that _name.projectedValue
is equal to $name
. It’s the State
struct, which you “get” via @State
, that manages the storage, retrieval and updating of the mere name
property. Neat!
You can check out the official documentation for the State struct, and see that it’s really not magic at all. You can see the wrapped value, the projected value with Binding
, and even a way to set an initial value for a @State
property.
@State and Bindings
So far we’ve only discussed how @State
wraps a property, and how SwiftUI subsequently manages the storage of that property. The State
struct, however, has an interesting component too: a binding.
Like its name implies, a binding lets you bind a value to something else. In our example, that’s the TextField
. It cannot be simpler than that: you provide $name
to the TextField
, and whenever the text in the TextField
changes, so does the value of the name
property.
Here’s the relevant code once more:
struct ContentView: View
{
@State private var name: String = "Bob and Alice"
var body: some View {
VStack {
Text("Hello, \(name)!")
TextField("Name", text: $name)
}
}
}
See that TextField("Name", text: $name)
bit? That’ll “connect” the name
property to the text field. When the text in the text field changes, so does the string value of the name
property.
When we look closer at $name
, we see that its type is Binding<String>
. This Binding struct – itself a property wrapper, too – creates a two-way connection between a property that stores data, and a view that displays and changes that data.
With @State
, you really get 2 property wrappers for the price of 1. There’s State
, which manages storage of data. And there’s Binding
, which creates a connection between the data storage and the view.
It makes you wonder: Does a TextField
manage its own state? As far as the value in the text field goes, no! The binding connects the TextField
with some state. In our case, the name
property.
This leads us to the last important principle when working with SwiftUI: a single source of truth. You’ll come across that term often when working with SwiftUI.
A source of truth is simply the “one place” where state is stored. Because SwiftUI views depend on state, you cannot have 2 copies of the same data. If there’s 2 copies, which one will be used to update the view? No, you can only have a single source of truth.
In the example code we’re using in this tutorial, the single source of truth is the name
property. As far as the TextField
is concerned, that’s where we’re storing the name for the greeting in the app. This is why a binding is needed. You need a way to connect the TextField
to a place to store data.
Author’s Note: There’s more to @Binding
, and property wrappers like @ObservedObject
, than we’re discussing now. Learn more in @ObservedObject and Friends in SwiftUI and in Working with @Binding in SwiftUI.
Further Reading
Pfew! Who knew how much complexity is hidden behind a simple attribute like @State
. You know what the cool thing is? You can forget everything you just learned, and simply work with @State
for what it is.
When the property annotated with @State
changes, the view updates too. And you can even bind that property to some other UI component, like TextField
, and have it magically get updated when the text field changes. Even though it isn’t magic, it sure feels like it. Awesome!
Want to learn more? Check out these resources:
- @ObservedObject and Friends in SwiftUI
- Working with @Binding in SwiftUI
- Get Started with SwiftUI for iOS
- Create UIs with Views and Modifiers in SwiftUI
- Working with Stacks in SwiftUI
- How To: Xcode 12 Tutorial for Beginners
- Understanding Model-View-Controller (MVC) on iOS
- The “Some” Keyword In Swift
- How To: Pass Data Between Views with SwiftUI