BS
BleepingSwift
Published on

> @State vs @Binding in SwiftUI: When to Use Each

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

SwiftUI uses property wrappers to manage data and trigger view updates. @State and @Binding are two of the most common, and understanding when to use each is fundamental to building SwiftUI apps correctly.

@State: Owning the Truth

Use @State when a view owns the data and is the source of truth for that value:

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

The CounterView creates and owns the count variable. When count changes, SwiftUI automatically re-renders the view. The @State property wrapper stores the value outside the view's struct (since structs are value types and get recreated), ensuring it persists across view updates.

Key characteristics of @State:

  • The view owns the data
  • Changes trigger view updates
  • Should be marked private since the data is internal to the view
  • Typically used for simple value types (Int, String, Bool, etc.)

@Binding: Borrowing the Truth

Use @Binding when a view needs to read and write a value owned by another view:

struct ToggleRow: View {
    let title: String
    @Binding var isOn: Bool

    var body: some View {
        Toggle(title, isOn: $isOn)
    }
}

struct SettingsView: View {
    @State private var notificationsEnabled = true
    @State private var darkModeEnabled = false

    var body: some View {
        Form {
            ToggleRow(title: "Notifications", isOn: $notificationsEnabled)
            ToggleRow(title: "Dark Mode", isOn: $darkModeEnabled)
        }
    }
}

SettingsView owns the state with @State. It passes a binding to ToggleRow using the $ prefix. ToggleRow can read and modify the value, but it doesn't own it—changes flow back to the parent.

Key characteristics of @Binding:

  • The view does not own the data
  • Creates a two-way connection to the source of truth
  • Changes made through the binding update the original value
  • Not marked private since it must be passed in from outside

The $ Prefix

The $ prefix creates a binding from a state variable:

@State private var name = ""

// $name is a Binding<String>
TextField("Name", text: $name)

Without the $, you're accessing the value itself. With $, you're accessing a binding that can both read and write.

Data Flow Pattern

The typical pattern flows downward: parent views own state and pass bindings to children.

struct ParentView: View {
    @State private var text = ""  // Owns the data

    var body: some View {
        ChildView(text: $text)    // Passes a binding
    }
}

struct ChildView: View {
    @Binding var text: String     // Borrows the data

    var body: some View {
        TextField("Enter text", text: $text)
    }
}

When the user types in the TextField, changes propagate through the binding back to ParentView's @State, which triggers both views to update.

Constant Bindings for Previews

When building previews or testing, use .constant() to create a read-only binding:

#Preview {
    ToggleRow(title: "Test", isOn: .constant(true))
}

The view will display correctly, but toggling won't change anything since the binding always returns the same value.

Common Mistakes

Declaring @Binding when you should use @State:

// Wrong: View doesn't receive this from outside
struct BadView: View {
    @Binding var count: Int  // Where does this come from?

    var body: some View {
        Text("\(count)")
    }
}

// Right: View owns this data
struct GoodView: View {
    @State private var count = 0

    var body: some View {
        Text("\(count)")
    }
}

Forgetting the $ when a binding is expected:

// Wrong: Passing the value, not a binding
TextField("Name", text: name)

// Right: Passing a binding
TextField("Name", text: $name)

Binding to Specific Properties

You can create bindings to properties of a larger state object:

struct User {
    var name: String
    var email: String
}

struct ProfileView: View {
    @State private var user = User(name: "", email: "")

    var body: some View {
        Form {
            TextField("Name", text: $user.name)
            TextField("Email", text: $user.email)
        }
    }
}

The $user.name creates a Binding<String> that reads and writes just the name property.

Custom Bindings

Create custom bindings for transformed or computed values:

struct SliderView: View {
    @State private var value: Double = 50

    var body: some View {
        VStack {
            Slider(value: $value, in: 0...100)
            Text("Value: \(Int(value))")
        }
    }

    // Custom binding that clamps the value
    var clampedBinding: Binding<Double> {
        Binding(
            get: { value },
            set: { newValue in
                value = min(max(newValue, 10), 90)
            }
        )
    }
}

Choosing Between Them

Ask yourself: "Who owns this data?"

If the view creates the data and no parent cares about it, use @State:

@State private var isExpanded = false  // Local UI state

If a parent view needs to know about or control the value, use @Binding:

@Binding var selectedTab: Int  // Parent controls which tab is selected

For app-wide state that multiple unrelated views need to access, consider @Observable (iOS 17+) or @EnvironmentObject instead. @State and @Binding work best for state that flows through the view hierarchy in a predictable parent-to-child pattern.

Quick Reference

ScenarioUse
Local toggle/counter@State
Reusable form field@Binding
Sheet presented state@State in parent
Child needs to modify parent data@Binding
Preview for binding component.constant()

The general rule: put @State as close to where the data is used as possible, and only lift it up when children need to modify it. This keeps your data flow simple and your views easy to reason about.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.