Back to blog

The @State Property Wrapper in SwiftUI Explained


Aasif Khan
By Aasif Khan | Last Updated on February 17th, 2024 7:22 am | 4-min read

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 app development 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

Describe your app idea
and AI will build your App

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 @IBOutlet 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:

  1. The @State property wrapper is added to the name property, and the property is made private. The view now depends on the state of name, and updates when name does.
  2. The TextField‘s text 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 the name 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:

  1. The @State property makes the view dependent on the state of name, so whenever the text value of name changes, the view is updated to show that change.
  2. The TextField receives a binding to the name property, which enables the text field to change the text value of name. 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:

  1. name is the (actual) wrapped value of the property, i.e. “Bob and Alice”
  2. _name is the property wrapper struct itself, which for @State is State
  3. $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.

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. 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!


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts