Swift and SwiftUI tutorials for Swift Developers

@Binding vs @State in SwiftUI

In the vast universe of Swift programming, the transition from UIKit to SwiftUI marked a fundamental paradigm shift: we stopped manually modifying views and started declaring states that dictate how those views should look. For any modern iOS developer, understanding state management is not optional; it is the most critical skill for surviving in the Apple ecosystem.

If you open Xcode today and create a SwiftUI project, you will immediately encounter two keywords that seem magical: @State and @Binding. At first glance, both seem to make the UI update, but confusing them is the number one source of bugs, erratic behaviors, and spaghetti architecture in iOS, macOS, and watchOS applications.

In this tutorial, we will dissect the anatomy of @State and @Binding in SwiftUI. You will not only learn what they are, but how they work “under the hood,” their architectural differences, and best practices for developing robust and scalable applications.


Part 1: The Owner of the Truth: What is @State?

To understand @State, we first need to understand how a View works in SwiftUI. Unlike UIView in UIKit (which are classes and persistent objects), a View in SwiftUI is a structure (struct). It is a value type, ephemeral and lightweight. It is created and destroyed thousands of times.

So, if the structure is constantly destroyed and recreated, where is the data saved? If you have a counter at 5, and the view redraws, why doesn’t it go back to 0?

Technical Definition

@State is a property wrapper that tells SwiftUI to reserve memory in the “heap” to store a value, outside the lifecycle of the view structure.

When you mark a property with @State, you are saying: “I, this View, am the absolute owner of this data. It is my Source of Truth.”

The @State Lifecycle

  1. Initialization: SwiftUI allocates storage for the variable.
  2. Modification: When you change the value of a @State variable, SwiftUI detects the change.
  3. Reaction: SwiftUI invalidates the current view and recalculates the body property.
  4. Rendering: The UI updates to reflect the new state.

Practical Example in Xcode

Imagine a simple counter. This is the “Hello World” of state.

import SwiftUI

struct CounterView: View {
    // @State declares property: "This view owns this integer"
    // It is private because no one else should touch the source of truth directly
    @State private var count: Int = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("Counter: \(count)")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            HStack {
                Button("Subtract") {
                    // Modifying state triggers the redraw
                    count -= 1
                }
                .buttonStyle(.bordered)
                
                Button("Add") {
                    count += 1
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
    }
}

Golden Rules for @State

  1. Always Private: Declare your @State properties as private. reinforce the idea that this state belongs exclusively to that view.
  2. Simple Types: @State is designed for simple value types: Int, String, Bool, or small structs. Do not use it for complex objects or heavy business logic (for that, @StateObject or @Observable exist).
  3. Locality: Use it for states that only matter to the local user interface (e.g., if a button is highlighted, if a menu is expanded, or the text of an input field).

Part 2: The Connector: What is @Binding?

If @State is the owner of the house, @Binding is the person who has a copy of the keys.

In a clean Swift programming architecture, we usually divide our large screens into smaller, reusable Child Views. Often, these child views need to control a value that belongs to the parent view.

Here arises the problem: structs in Swift are value types. If you pass a simple variable to the child view, Swift passes a copy. If the child changes its copy, the parent never finds out.

Technical Definition

@Binding is a property wrapper that creates a read and write connection (bidirectional) between a view and a source of truth residing elsewhere.

It does not own the data. It is simply a reference, a safe pointer that says: “I don’t have the data, but I have permission to read and modify it at the original source.”

The Light Switch Metaphor

Imagine the electrical wiring in your house.

  • @State: Is the light bulb and the actual wiring in the ceiling (the source of the light).
  • @Binding: Is the switch on the wall. The switch doesn’t “have” light inside it, but it controls the bulb located elsewhere.

Practical Example: Reusable Component

Let’s extract the buttons from the previous example into a reusable view.

struct ControlPanel: View {
    // @Binding declares: "Someone else will pass this to me, and I will modify it"
    // It has no initial value (no = 0), because it does not own the data.
    @Binding var value: Int

    var body: some View {
        HStack {
            Button("-") {
                value -= 1 // This modifies the parent's @State
            }
            .padding()
            .background(Color.red.opacity(0.2))
            .cornerRadius(8)
            
            Button("+") {
                value += 1 // This also travels upwards
            }
            .padding()
            .background(Color.green.opacity(0.2))
            .cornerRadius(8)
        }
    }
}

How do we connect this? Using the dollar sign ($).

struct MainParentView: View {
    @State private var score = 0 // The Source of Truth

    var body: some View {
        VStack {
            Text("Score: \(score)")
            
            // We pass the Binding using $
            // This creates the "cable" between score and value
            ControlPanel(value: $score)
        }
    }
}

Part 3: @State and @Binding in SwiftUI – Differences and Similarities

For an iOS developer, knowing how to write code is only half the battle; understanding architecture is what allows you to scale. Let’s compare both tools head-to-head.

Similarities

  1. UI Triggers: Both cause the view to invalidate and redraw when the value changes. They are the engine of reactivity in SwiftUI.
  2. Property Wrappers: Both use the @ syntax and project a value (projectedValue) that allows binding with other components (like TextField or Toggle).
  3. Type Safety: Both leverage Swift’s strong typing. You cannot bind a Binding<String> to a State<Int>.

Key Differences

Feature @State (The Owner) @Binding (The Connector)
Data Ownership Owns and stores the data in memory. Owns nothing; only references elsewhere.
Initialization Must be initialized with a value (= 0, = false). Has no default value; injected when initializing the view.
Scope Generally private. Local to the view. Generally internal or public. It is the child view’s API.
Source Is the Source of Truth. Is a derivation or connection.
Persistence Survives view redraws. Depends on the life of the original source.

Part 4: Complex Scenario: Data Flow in iOS, macOS, and watchOS

One of the beauties of SwiftUI is that the code you write to manage @State and @Binding works identically on an iPhone, a Mac, or an Apple Watch.

Let’s analyze a common pattern: A user profile form. This is a scenario where many junior developers fail by mixing concepts.

The Problem

We want a main screen that shows the user’s name and a button to edit it. Clicking edit opens a Sheet with a text field.

The Architectural Solution

  1. Main View: Owns the data (@State name).
  2. Edit View: Receives a link (@Binding name) to modify it directly.
import SwiftUI

// 1. The Child View (Form)
struct EditProfileView: View {
    @Binding var username: String // Link to original data
    @Binding var isPresented: Bool // Link to close the modal
    
    var body: some View {
        NavigationStack {
            Form {
                Section("Public Information") {
                    // TextField requires a Binding, and we already have one
                    TextField("Username", text: $username)
                }
            }
            .navigationTitle("Edit Profile")
            .toolbar {
                Button("Done") {
                    isPresented = false // Modifies the parent's state to close
                }
            }
        }
    }
}

// 2. The Parent View (Main Screen)
struct ProfileDashboard: View {
    @State private var currentName: String = "DevSwift2024"
    @State private var showSheet: Bool = false
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "person.crop.circle.fill")
                .font(.system(size: 100))
                .foregroundColor(.blue)
            
            Text("Hello, \(currentName)")
                .font(.title2)
            
            Button("Edit Profile") {
                showSheet = true
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .sheet(isPresented: $showSheet) {
            // Here we pass the Bindings
            // $currentName passes the ability to write to currentName
            // $showSheet passes the ability to close the sheet
            EditProfileView(username: $currentName, isPresented: $showSheet)
        }
    }
}

Code Analysis

Notice the elegance of this pattern. EditProfileView doesn’t need to know where the name comes from, nor if it is saved in a database or in memory. It only knows it has permission to edit a String. This makes EditProfileView highly testable and reusable.


Part 5: Common Mistakes and Best Practices

Even Swift programming experts make mistakes with these wrappers. Here is a checklist for your Xcode projects.

1. Initializing @State with external values

This is the most common mistake.

struct BadView: View {
    var initialTitle: String
    @State private var title: String

    init(text: String) {
        self.initialTitle = text
        // BAD: This will only work the FIRST time the view is created.
        // If the parent changes 'text', the @State will NOT reinitialize.
        _title = State(initialValue: text) 
    }
}

Solution: If the value comes from outside and can change, use @Binding, not @State! @State is only for initial internal storage.

2. Forgetting the private modifier

If you don’t mark your @State as private, you are inviting other developers (or yourself in the future) to try injecting that state from outside via the memberwise initializer. This breaks encapsulation and the “Single Source of Truth” principle.

3. Using @State for Complex Data Models

If you have a User class with 20 properties and business logic, do not use @State.

  • Use @State for simple values (local UI).
  • Use @StateObject (pre-iOS 17) or the @Observable macro (iOS 17+) for complex data models. @State is not optimized to observe deep changes within classes.

Conclusion

Mastering @State and @Binding in SwiftUI is the first big step to becoming a senior iOS developer. These two tools form the backbone of data flow in your applications.

Always remember the golden rule:

  • Ask yourself: “Who owns this data?”
  • If the answer is “Me, this view”, use @State.
  • If the answer is “Someone else, I just edit it”, use @Binding.

By applying these concepts correctly in Xcode, you will notice that your iOS, macOS, and watchOS applications become more predictable, with fewer synchronization errors and much easier to maintain. The magic of SwiftUI lies in its declarative simplicity; don’t fight against it, flow with the state.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

@Binding vs @Bindable in SwiftUI

Next Article

How to parse JSON in SwiftUI

Related Posts