Inside SwiftUI @State property wrapper

Sasha Myshkina
5 min readSep 12, 2023

--

This article is part 1 of the upcoming series about SwiftUI’s states and bindings. Though none of us has got an exclusive access to the SwiftUI source code, it’s always enthralling to try to take a peek at the details of the SwiftUI essentials. Let’s drill down together!

Photo by Max Duzij on Unsplash

What is @State?

@State is a property wrapper that should be used for private state value changes inside the view. Consider using @State for the property declared inside the view. @State property is meant to be changed or/and displayed inside the view. The main rule is to use @State only for storage that is local to a view and its subviews.

Let’s look at a typical scenario @State property can be used:

import SwiftUI

// This is an example of the basic @State property wrapper usage
struct ContentView: View {

// MARK: - Properties

@State var count: Int = 0

// MARK: - Body

var body: some View {
Button(action: {
count += 1
}, label: {
buttonContent
})
}


private var buttonContent: some View {
HStack {
Image(systemName: "arrow.up")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Count: \(count)")
.foregroundStyle(.white)
.padding(.horizontal)
}
.padding()
.background {
Color.black
.cornerRadius(16)
}
}
}

Here I declare @State variable count that has initial value 0. Every time on tapping a button the count variable will be incremented by 1. This is how we set the value. Also, the button title will be the one to reflect this variable changes — we are interpolating the title with the value from the count @State property.

But now, let’s think more about the magic of the @State variables in SwiftUI. How can the value property be changed, i.e mutated if the SwiftUI view is a struct which is not a reference type but a value type? And how do we change the value this way inside the Button’s closure that is not a mutable context? Let’s try to remove the @State property wrapper and see the error the compiler gives us.

⚠️ Left side of mutating operator isn’t mutable: ‘selfis immutable

Obviously, self indeed is immutable. So how could it work at all then?
The answer lies inside the State<Value> struct provided by Apple. To replicate the property wrapper, we can use a State value for the count property. To write the count property, we will need to do it like that:

struct ContentView: View {

// MARK: - Properties

private var count = State(initialValue: 0)

// MARK: - Body

var body: some View {
Button(action: {
count.wrappedValue += 1
}, label: {
buttonContent
})
}


private var buttonContent: some View {
HStack {
Image(systemName: "arrow.up")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Count: \(count.wrappedValue)")
.foregroundStyle(.white)
.padding(.horizontal)
}
.padding()
.background {
Color.black
.cornerRadius(16)
}
}
}

Looks a bit bulky, doesn’t it? Let’s try to create a computed property to avoid using count.wrappedValue each and every time.

struct ContentView: View {

// MARK: - Properties

private var count = State(initialValue: 0)

private var _count: Int {
get {
count.wrappedValue
}
set {
// compiler error here
count.wrappedValue = newValue
}
}

// MARK: - Body

var body: some View {
Button(action: {
_count += 1
}, label: {
buttonContent
})
}


private var buttonContent: some View {
HStack {
Image(systemName: "arrow.up")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Count: \(_count)")
.foregroundStyle(.white)
.padding(.horizontal)
}
.padding()
.background {
Color.black
.cornerRadius(16)
}
}
}

But when setting the _count.wrappedValue, the compiler again gives us the same error.

⚠️ Left side of mutating operator isn’t mutable: ‘selfis immutable

How come it can be mutable inside the Button’s action closure, but gives an error when assigning a value inside set? To get close to revealing this secret, we must look into what is the wrappedValue itself:

public var wrappedValue: Value { get nonmutating set }

So setting wrappedValue is possible because nonmutating set is used. This is an example that will compile without a hitch:

import SwiftUI

struct ContentView_StateUnderTheHood: View {

// MARK: - Properties

private var _count = State(initialValue: 0)

private var count: Int {
get {
_count.wrappedValue
}
nonmutating set {
_count.wrappedValue = newValue
}
}

// MARK: - Body

var body: some View {
Button(action: {
count += 1
}, label: {
buttonContent
})
}


private var buttonContent: some View {
HStack {
Image(systemName: "arrow.up")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Count: \(count)")
.foregroundStyle(.white)
.padding(.horizontal)
}
.padding()
.background {
Color.black
.cornerRadius(16)
}
}
}

Here, we can think of nonmutating as a trick around standard value type mutation. Value types are immutable, and to change the value we need to use the mutating keyword (think mutating functions in structs).

All in all, when using the wrappedValue of the State property count, what it really has to do with is a reference to the value, as a pointer to the actual value of the state property.

So the @State property wrapper will do it all for us for free. It creates both the _count and the count properties. And it allows to have both initial value (which is 0) and the wrappedValue we are interacting with by hitting the button and incrementing the count.

Passing value to the @State var in init

Another interesting part is trying to pass a value to the @State property count from the outside. Simply passing Int value will leave the compiler happy, but are we happy?

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
// Passing 1 to the init
ContentView(count: 1)
}
}
}

struct ContentView: View {

// MARK: - Properties

@State var count: Int = 0


// MARK: - Init
// Despite it's compiled, the button's title will be unchanged
// showing the "Count: 0" text
init(count: Int) {
self.count = count
}
}

Despite this code is compiled, the result is unsatisfactory. We are setting the initial value to be 1 from the outside. However, when the button appears on the screen, its title says “Count: 0”.

struct ContentView: View {

// MARK: - Properties

@State var count: Int = 0


// MARK: - Init
// Initialising State property inside the init will ensure
// count value is passed correctly and the button shows the right title
init(count: Int) {
_count = State(initialValue: count)
}
}

The reason lies inside the essence of the SwiftUI itself. When this initialiser runs, the view doesn't have an identity yet. But when it does — it will show the initial value of the state property, which is 0.

Though in some cases initialising the @State property may seem reasonable, let me emphasise it once again: @State is used for the property declared inside the view. And it is very natural for it to be initialised, changed, and displayed inside the view.

In the next articles, I am going to cover @Binding, @StateObject, and @ObservableObject, as well as some very exciting changes to them brought by iOS 17.

Subscribe and stay tuned. Happy coding!

--

--