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
- Own state at the lowest common ancestor -- the view that needs to share it
- Use bindings for form inputs -- text fields, toggles, sliders
- Use callbacks for events -- button taps, completion handlers
- Keep one direction for state: parent -> child via bindings, child -> parent via callbacks
- 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?
2. What is Binding.constant used for?
3. When should you use a callback closure instead of a binding?
4. How do you create a binding with custom get/set logic?
5. What property wrapper creates bindings from @Observable objects?
Flashcards
Question
What is a Binding?
Click to reveal answer
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?
Click to reveal answer
Answer
Binding.constant(value) -- the getter returns the fixed value and the setter is ignored.
Question
When do you use Binding(get:set:)?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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.