Skip to content
intermediate Phase 2 · SwiftUI Fundamentals

Bindings & Two-Way Data Flow

Create and pass bindings between parent and child views for two-way data synchronization.

40m
2 problems
Topic Progress 0%

Creating Bindings

What Is a Binding?

A Binding is a reference to a value that provides read and write access to that value. It acts as a two-way bridge between a view that owns state and a child view that needs to read and modify it.

struct ParentView: View {
    @State private var name = ""
    
    var body: some View {
        // $name creates a Binding<String>
        ChildView(name: $name)
    }
}

struct ChildView: View {
    @Binding var name: String
    
    var body: some View {
        TextField("Enter name", text: $name)
    }
}

The $ prefix on a @State property produces a Binding<Value>. The child receives it as a @Binding var and can both read and write the value.

Binding from @State

The most common pattern is creating bindings from @State properties:

struct FormView: View {
    @State private var username = ""
    @State private var password = ""
    @State private var rememberMe = false
    
    var body: some View {
        VStack {
            TextField("Username", text: $username)
            SecureField("Password", text: $password)
            Toggle("Remember me", isOn: $rememberMe)
        }
    }
}

Each $ access creates a new Binding that points to the underlying @State storage.

Binding from @Observable Objects

With @Observable (iOS 17+), use @Bindable to create bindings:

@Observable
class Settings {
    var darkMode = false
    var notifications = true
    var fontSize: Double = 16
}

struct SettingsView: View {
    @Bindable var settings: Settings
    
    var body: some View {
        Form {
            Toggle("Dark Mode", isOn: $settings.darkMode)
            Toggle("Notifications", isOn: $settings.notifications)
            Slider(value: $settings.fontSize, in: 12...24, step: 1)
        }
    }
}

Binding from @EnvironmentObject

struct ProfileEditor: View {
    @EnvironmentObject var profile: UserProfile
    
    var body: some View {
        Form {
            TextField("Name", text: $profile.name)
            Toggle("Public Profile", isOn: $profile.isPublic)
        }
    }
}

Multiple Bindings in One View

A view can receive multiple bindings for different aspects of state:

struct ColorPicker: View {
    @Binding var red: Double
    @Binding var green: Double
    @Binding var blue: Double
    
    var body: some View {
        VStack {
            Color(red: red/255, green: green/255, blue: blue/255)
                .frame(width: 100, height: 100)
            
            Slider(value: $red, in: 0...255)
                .tint(.red)
            Slider(value: $green, in: 0...255)
                .tint(.green)
            Slider(value: $blue, in: 0...255)
                .tint(.blue)
        }
    }
}

// Parent
struct ContentView: View {
    @State private var red: Double = 128
    @State private var green: Double = 128
    @State private var blue: Double = 128
    
    var body: some View {
        ColorPicker(red: $red, green: $green, blue: $blue)
    }
}

Binding Variables

Binding.constant

Create a read-only binding that always returns a fixed value:

// Always shows "Preview" -- writes are ignored
TextField("Name", text: .constant("Preview"))

// Useful for previews
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        FormView()
            .environmentObject(Settings(name: "Preview User"))
    }
}

Binding.constant is also useful for disabling interactions:

Button("Submit") { submit() }
    .disabled(.constant(true))  // always disabled

Binding(get:set:)

Create a binding with custom getter and setter logic:

struct TemperatureView: View {
    @State private var celsius: Double = 20
    
    var body: some View {
        VStack {
            // Binding that converts between Celsius and Fahrenheit
            Slider(
                value: Binding(
                    get: { celsius * 9/5 + 32 },
                    set: { celsius = ($0 - 32) * 5/9 }
                ),
                in: 32...212
            )
            Text("\(Int(celsius))°C / \(Int(celsius * 9/5 + 32))°F")
        }
    }
}

Binding with Validation

struct LimitedTextField: View {
    @Binding var text: String
    let maxLength: Int
    
    var body: some View {
        TextField("Enter text", text: Binding(
            get: { text },
            set: { newValue in
                if newValue.count <= maxLength {
                    text = newValue
                }
            }
        ))
    }
}

// Usage
@State private var bio = ""
LimitedTextField(text: $bio, maxLength: 150)

Binding with Optional Values

struct SearchView: View {
    @State private var searchText: String = ""
    
    var body: some View {
        NavigationView {
            List {
                ForEach(results) { item in
                    Text(item.name)
                }
            }
            .searchable(
                text: $searchText,
                prompt: "Search items"
            )
        }
    }
}

Creating Bindings Programmatically

You can create bindings in helper functions:

func makeLimitedBinding(_ source: Binding<String>, maxLength: Int) -> Binding<String> {
    Binding(
        get: { source.wrappedValue },
        set: { newValue in
            if newValue.count <= maxLength {
                source.wrappedValue = newValue
            }
        }
    )
}

// Usage
struct ContentView: View {
    @State private var name = ""
    
    var body: some View {
        TextField("Name", text: makeLimitedBinding($name, maxLength: 20))
    }
}

Two-Way Communication

Parent-to-Child Flow

The parent passes state down via bindings. The child reads the value without owning it:

struct UserProfile: View {
    @Binding var name: String
    
    var body: some View {
        VStack {
            Text("Hello, \(name)")
            TextField("Edit name", text: $name)
        }
    }
}

Child-to-Parent Flow

The child writes to the binding, and the parent's state updates automatically:

struct ColorAdjuster: View {
    @Binding var brightness: Double
    @Binding var contrast: Double
    
    var body: some View {
        VStack {
            Button("Brighten") { brightness += 0.1 }
            Button("Darken") { brightness -= 0.1 }
            Button("Boost Contrast") { contrast += 0.1 }
        }
    }
}

// Parent
struct PhotoEditor: View {
    @State private var brightness = 0.5
    @State private var contrast = 0.5
    
    var body: some View {
        VStack {
            PhotoPreview(brightness: brightness, contrast: contrast)
            ColorAdjuster(brightness: $brightness, contrast: $contrast)
        }
    }
}

Event Callbacks as Alternative

For one-way child-to-parent communication (events, not state), closures are cleaner:

struct ConfirmDialog: View {
    let message: String
    let onConfirm: () -> Void
    let onCancel: () -> Void
    
    var body: some View {
        VStack {
            Text(message)
            HStack {
                Button("Cancel", role: .cancel) { onCancel() }
                Button("Confirm", role: .destructive) { onConfirm() }
            }
        }
    }
}

// Usage
ConfirmDialog(
    message: "Delete this item?",
    onConfirm: { delete() },
    onCancel: { dismiss() }
)

Bindings with Animations

Bindings automatically participate in SwiftUI animations:

struct AnimatedToggle: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            Button("Toggle") {
                withAnimation(.spring(response: 0.3)) {
                    isExpanded.toggle()
                }
            }
            
            if isExpanded {
                Text("Expanded content")
                    .transition(.slide)
            }
        }
    }
}

Best Practices for Data Flow

  1. Own state at the lowest common ancestor -- the view that needs to share it
  2. Use bindings for form inputs -- text fields, toggles, sliders
  3. Use callbacks for events -- button taps, completion handlers
  4. Keep one direction for state: parent -> child via bindings, child -> parent via callbacks
  5. Avoid passing too many bindings -- if a view needs 5+ bindings, consider a view model

Quiz

1. What does the $ prefix create when used with @State?

Question 1 options

2. What is Binding.constant used for?

Question 2 options

3. When should you use a callback closure instead of a binding?

Question 3 options

4. How do you create a binding with custom get/set logic?

Question 4 options

5. What property wrapper creates bindings from @Observable objects?

Question 5 options

Flashcards

Question

What is a Binding?

Answer

A reference to a value that provides two-way read/write access, connecting a parent's state to a child view.

Question

How do you create a read-only binding?

Answer

Binding.constant(value) -- the getter returns the fixed value and the setter is ignored.

Question

When do you use Binding(get:set:)?

Answer

When you need custom logic in the getter or setter, such as type conversion, validation, or filtering.

Question

What is the difference between a binding and a callback?

Answer

A binding is two-way state synchronization. A callback is a one-way notification from child to parent.

Question

What property wrapper creates bindings from @Observable objects?

Answer

@Bindable, used alongside .environment() injection.

Revision Notes

Key Takeaways

  • 1. $value creates a Binding from @State properties
  • 2. Binding.constant provides a read-only binding for previews and disabled states
  • 3. Binding(get:set:) enables custom transformation and validation logic
  • 4. Use bindings for two-way state and closures for one-way events
  • 5. @Bindable creates bindings from @Observable classes (iOS 17+)

Interview Tips

  • Explain the difference between a Binding and a direct value reference
  • Know when to use Binding(get:set:) for type conversion or validation
  • Be ready to discuss binding patterns for parent-child communication
  • Understand the distinction between state flow (bindings) and event flow (closures)

Cheat Sheet

Binding provides two-way read/write access to state.
$prefix on @State creates a Binding.
Binding.constant: read-only, fixed value.
Binding(get:set:): custom getter/setter logic.
@Bindable: creates bindings from @Observable objects.
Use bindings for state flow; use closures for events.
Own state at lowest common ancestor.