Swift and SwiftUI tutorials for Swift Developers

@Binding vs @Bindable in SwiftUI

In the Swift programming ecosystem, state management has always been the beating heart of our applications. With the arrival of SwiftUI, we moved from manually synchronizing UI with data to a declarative paradigm where the view is a direct function of state.

For any iOS developer who started with SwiftUI before 2023, @Binding was their daily bread. However, with the release of iOS 17, Swift 5.9, and the revolution of the Observation framework, a new player appeared in Xcode: @Bindable.

Confusion is common: Are they the same? Does one replace the other? When should I use which? In this deep-dive tutorial, we will break down the anatomy of @Bindable and @Binding in SwiftUI, exploring their similarities, differences, and how to use them to create robust applications on iOS, macOS, and watchOS.


Part 1: The Indispensable Veteran: What is @Binding?

To understand the present, we must understand the foundations. @Binding has been with us since the first version of SwiftUI.

Technical Definition

@Binding is a property wrapper that creates a two-way connection for reading and writing between a view and a source of truth that resides elsewhere.

Imagine @Binding as a “phone line”. The view holding the @Binding does not own the data; it doesn’t store it in memory. It simply has a direct line to read the original value and send changes back to the owner of that data.

When is @Binding used?

The classic use case is the Parent-Child relationship.

  1. The Parent View owns the state (using @State).
  2. The Child View needs to display that state AND modify it (e.g., a Toggle, a TextField, or a Slider).

Practical Example in Xcode

Let’s imagine a simple “Airplane Mode” setting in an iOS app.

import SwiftUI

// CHILD VIEW (Reusable Component)
struct AirplaneModeSwitch: View {
    // @Binding indicates: "I don't own this boolean, but I can change it"
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Airplane Mode", isOn: $isOn) // The $ sign projects the Binding
            .padding()
            .background(isOn ? Color.orange.opacity(0.1) : Color.clear)
            .cornerRadius(10)
    }
}

// PARENT VIEW (Owner of the truth)
struct SettingsView: View {
    // @State indicates: "I am the owner of this data"
    @State private var isAirplaneModeEnabled = false

    var body: some View {
        VStack {
            Text("Network Settings")
                .font(.headline)
            
            // We pass the reference with $ to create the Binding
            AirplaneModeSwitch(isOn: $isAirplaneModeEnabled)
            
            Text(isAirplaneModeEnabled ? "Radio off" : "Searching for signal...")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

Keys for the iOS Developer:

  • Syntax: Passed with the $ prefix from the parent.
  • Value Types: Works excellently with simple value types (Bool, String, Int) and Structs.
  • Limitation: If you start passing a @Binding through 5 levels of view hierarchy, your code will start to smell (Prop Drilling).

Part 2: The Observation Revolution (Necessary Context)

Before talking about @Bindable, we must talk about the seismic shift that occurred in Swift programming at WWDC 2023.

Apple introduced the Observation framework. Previously, we used ObservableObject and @Published (Combine). Now, we simply use the @Observable macro.

  • The Old World (ObservableObject): To observe an object, the view needed @ObservedObject or @StateObject.
  • The New World (@Observable): SwiftUI views automatically detect when a property of a class marked with @Observable is read in the body, and redraw when it changes. We no longer need @ObservedObject. We pass classes as normal parameters (let model: User).

But a problem arose: If I pass an object as a simple let parameter, how do I create Bindings to its properties for my UI controls?

This is where @Bindable comes in.


Part 3: The New Challenger: What is @Bindable?

@Bindable is a property wrapper designed specifically to work with reference types (classes) that use the new @Observable macro.

The Problem It Solves

In the new Observation system, you often have a model object:

@Observable
class UserProfile {
    var name: String = ""
    var age: Int = 18
}

If you pass this object to a view: let profile: UserProfile. You cannot use $profile.name to bind it to a TextField. Why? Because profile is a constant (let) and does not have a native binding projector like @State or @ObservedObject did.

The Solution with @Bindable

@Bindable takes that observable object and creates the necessary bindings so that UI controls (TextField, Toggle, Picker) can write to the object’s properties.

Practical Example in Xcode

Let’s create a profile editing form.

import SwiftUI
import Observation

// DATA MODEL (@Observable Macro)
@Observable
class UserSettings {
    var username: String = "DevSwift2024"
    var isPrivateProfile: Bool = true
}

// EDIT VIEW
struct ProfileEditor: View {
    // Option A: Declare in params if the view MUST have bindings
    // @Bindable var settings: UserSettings 
    
    // Option B (Most common): Receive normal object and make it bindable inside
    var settings: UserSettings

    var body: some View {
        // Create the Bindable scope here
        @Bindable var bindableSettings = settings

        Form {
            Section("Information") {
                // Now we can use $bindableSettings
                TextField("Username", text: $bindableSettings.username)
            }
            
            Section("Privacy") {
                Toggle("Private Profile", isOn: $bindableSettings.isPrivateProfile)
            }
        }
    }
}

// MAIN VIEW
struct MainProfileView: View {
    @State private var settings = UserSettings()

    var body: some View {
        ProfileEditor(settings: settings)
    }
}

Deep Analysis

Notice the magic inside ProfileEditor. We receive var settings: UserSettings. It is a simple reference. But inside the body, we use: @Bindable var bindableSettings = settings.

This line “unlocks” the ability to use the $ sign to modify class properties in real-time.


Part 4: @Binding vs @Bindable – Head to Head

For an iOS developer, distinguishing when to use each is vital for app architecture. Let’s contrast @Bindable and @Binding in SwiftUI.

Similarities

  1. End Goal: Both share the same objective: allowing a UI element (like a TextField) to modify data it doesn’t directly own.
  2. UI Syntax: Both enable the use of the $ prefix to pass the write reference to SwiftUI controls.
  3. Data Flow: Both facilitate bidirectional data flow.

Key Differences

Feature @Binding @Bindable
Data Source Generally value types (struct, Bool, String) coming from a parent @State. Exclusively for reference types (Classes) using the @Observable macro.
Location Almost always in the View’s parameter definition (var body rarely declares bindings). Can be used in parameters, but shines when declared inside the body to unwrap an existing object.
Dependency Doesn’t care if the source is @State, @ObservedObject, or @Query (SwiftData). Only wants a read/write channel. Strictly depends on the Observation framework (iOS 17+).
Property “I have permission to touch this specific value”. “I have permission to create bindings to any property of this object”.

Part 5: Real World Scenarios for the iOS Developer

Let’s look at how to integrate both into a modern architecture using SwiftUI and Xcode.

Scenario 1: The List Cell (The Realm of @Bindable)

Imagine a task list (TodoItem) where each cell has a Toggle to mark it as completed. Using the new SwiftData or @Observable, this is trivial with @Bindable.

@Observable
class TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isDone: Bool
    
    init(title: String, isDone: Bool = false) {
        self.title = title
        self.isDone = isDone
    }
}

struct TodoListView: View {
    @State private var items = [
        TodoItem(title: "Learn @Bindable"),
        TodoItem(title: "Refactor code")
    ]

    var body: some View {
        List {
            // Pro Tip: When iterating with binding, we get direct access
            ForEach(items) { item in
                TodoCell(item: item)
            }
        }
    }
}

struct TodoCell: View {
    // 1. We receive the observable object
    var item: TodoItem
    
    var body: some View {
        // 2. We make it 'Bindable' to edit it
        @Bindable var bindableItem = item
        
        HStack {
            // 3. We use the binding ($)
            Toggle(isOn: $bindableItem.isDone) {
                Text(item.title)
                    .strikethrough(item.isDone)
            }
        }
    }
}

Scenario 2: Atomic Components (The Realm of @Binding)

Now imagine you want to create your own custom Checkbox component that is reusable for any boolean, not just for TodoItems. Here @Binding is still king.

struct CustomCheckbox: View {
    // It doesn't care if it's a TodoItem, UserSettings, or local State.
    // It just wants a boolean.
    @Binding var isChecked: Bool
    
    var body: some View {
        Button {
            isChecked.toggle()
        } label: {
            Image(systemName: isChecked ? "checkmark.square.fill" : "square")
                .font(.title)
        }
    }
}

// Usage in the previous cell:
// CustomCheckbox(isChecked: $bindableItem.isDone)

Architecture Lesson:

  • Use @Bindable in Screen Views or complex Cells that know your Data Model (Observable Classes).
  • Use @Binding in generic UI Components (Buttons, custom Inputs) that should be agnostic to the data model.

Part 6: Common Mistakes and Best Practices

As an iOS developer, optimizing your workflow in Xcode is vital. Here are common traps when using these tools.

1. Forgetting the @Observable Macro

@Bindable does not work with classes conforming to ObservableObject (the old protocol). If you try to use it with an old class, Xcode will throw cryptic errors. Make sure to migrate your models to the @Observable macro if your target is iOS 17+.

2. Using @Bindable when there is no editing

If you are only going to read data (display text), you do not need @Bindable.

// BAD (Unnecessary)
struct ReadOnlyView: View {
    var user: User
    var body: some View {
        @Bindable var bUser = user // Unnecessary overhead
        Text(bUser.name)
    }
}

// GOOD
struct ReadOnlyView: View {
    var user: User
    var body: some View {
        Text(user.name) // Swift knows how to detect the read and subscribe
    }
}

3. Binding.constant for Previews

When working in Xcode, you’ll often want to preview components that use @Binding without creating full state logic. Use .constant():

#Preview {
    AirplaneModeSwitch(isOn: .constant(true))
}

For @Bindable, simply pass an instance of the object in the preview, as the observation system works automatically.


Conclusion: The Future of SwiftUI

The introduction of @Bindable does not kill @Binding. On the contrary, they complete the circle of state management in Swift programming.

  • @Binding remains the fundamental glue for connecting parent and child components, especially for value types and pure UI components.
  • @Bindable is the modern tool we need to interact with our class-based data models and the Observation framework.

Mastering the distinction between @Bindable and @Binding in SwiftUI will allow you to write cleaner code, reduce boilerplate, and make the most of the performance capabilities of iOS, macOS, and watchOS. The next time you face a compilation error trying to pass an object to a TextField, remember: if it’s an Observable class, you need @Bindable. If it’s a simple value from the parent, you need @Binding.

Your journey as an iOS developer is a constant evolution. Embrace Observation, clean up your views, and let SwiftUI do the heavy lifting for you.

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

@ViewBuilder vs struct View in SwiftUI

Next Article

@Binding vs @State in SwiftUI

Related Posts