State and actions
SwiftUI: properties
Learn how properties hold data in SwiftUI views, from simple constants to State and Binding property wrappers that keep the UI in sync with data.
8 minute lesson
You can add any property to any SwiftUI view, like this:
import SwiftUI
struct ContentView: View {
let name = "Flavio"
var body: some View {
Text("Hello, \(name)!")
.font(.largeTitle)
}
}

See how I used let because the property is a constant.
Here is another example with an integer:
import SwiftUI
struct ContentView: View {
let name = "Flavio"
let age = 38
var body: some View {
VStack {
Text("Hello, \(name)!")
.font(.largeTitle)
Text("You are \(age) years old")
}
}
}

Constants are fine for data that never changes. Things get interesting when the data has to change.
Why a plain var doesn’t work
Suppose you want a counter that increases when the user taps a button. You might try this:
struct ContentView: View {
var count = 0
var body: some View {
Button("Taps: \(count)") {
count += 1 // does not compile
}
}
}
This doesn’t compile. Views are structs, and a struct can’t mutate its own properties from inside body.
And even if it could, it wouldn’t help. SwiftUI destroys and recreates your view structs constantly, every time something on screen needs updating. Any value stored in a plain property would be reset on the next refresh. Your changes would never stick.
@State
The fix is the @State property wrapper:
struct ContentView: View {
@State private var count = 0
var body: some View {
Button("Taps: \(count)") {
count += 1
}
}
}
@State tells SwiftUI: store this value for me, outside the view struct, and keep it alive across refreshes. When the value changes, SwiftUI re-renders the parts of the interface that depend on it.
Two conventions to follow. Mark @State properties private, because this is state the view owns and nobody else should touch directly. And use it for small value types: a String, an Int, a Bool.
Passing state to children with @Binding
A view often wants a child view to edit its state. Think of a settings screen that owns a Bool, and a reusable switch component that flips it.
The parent keeps @State. The child declares @Binding. To connect them, the parent prefixes the property with $:
struct ContentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "Enabled" : "Disabled")
PowerSwitch(isOn: $isOn)
}
}
}
struct PowerSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Power", isOn: $isOn)
}
}
The $ prefix gives you the projected value of the state property: a Binding. A binding is a read-write connection to the original value. When PowerSwitch flips the toggle, it writes straight into the parent’s isOn, and the Text above updates.
The rule of thumb: @State for state you own, @Binding for state passed to you.
What about bigger objects?
@State covers values local to one view. When you have a model class shared across many screens, you mark the class with the @Observable macro and pass instances around. The mental model stays the same: state you own versus state handed to you. We’ll get there in a later lesson. For now, @State and @Binding cover everything a small app needs.
Lesson completed