Passing Bindings Through Multiple View Layers
Goal
By the end of this tutorial, you will have built a three-layer SwiftUI flow where top-level state is edited safely from deep child views.
“Build a profile editor where
usernameis owned at the screen level and edited inside a nested row.”
Requirements
- Basic SwiftUI knowledge
- Xcode 15+
- iOS 17+
The use case
Large screens often contain nested sections and reusable rows. A common mistake is moving ownership into a child with @State just to make editing easier. We will keep ownership at the top and forward bindings through layers.
Step 1 - The simplest working version
Start with one owner and one editor.
import SwiftUI
struct ProfileScreen: View {
@State private var username = "Pascal"
var body: some View {
UsernameEditor(username: $username)
}
}
struct UsernameEditor: View {
@Binding var username: String
var body: some View {
TextField("Username", text: $username)
.textFieldStyle(.roundedBorder)
.padding()
}
}
This already has one source of truth.
Step 2 - Make it reusable
Introduce an intermediate section and forward the binding.
struct ProfileScreen: View {
@State private var username = "Pascal"
var body: some View {
ProfileSection(username: $username)
}
}
struct ProfileSection: View {
@Binding var username: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Account")
.font(.headline)
UsernameRow(username: $username)
}
.padding()
}
}
struct UsernameRow: View {
@Binding var username: String
var body: some View {
HStack {
Text("Username")
Spacer()
TextField("Username", text: $username)
.multilineTextAlignment(.trailing)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 180)
}
}
}
Each layer reflects or forwards; ownership still lives in ProfileScreen.
Step 3 - SwiftUI-specific refinement
Add validation and intent callbacks without moving ownership.
struct UsernameRow: View {
@Binding var username: String
let onSubmit: () -> Void
var body: some View {
TextField("Username", text: $username)
.textFieldStyle(.roundedBorder)
.onSubmit(onSubmit)
.overlay(alignment: .trailing) {
if username.count < 3 {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
}
}
}
}
This keeps editing (@Binding) and side effects (onSubmit) clearly separated.
Result
You now have a nested editing flow that is:
- reusable
- composable
- ownership-safe
Common SwiftUI pitfalls
-
Mistake: Child view declares
@Statefor shared form values
Why it happens: quick extraction without ownership check
How to fix it: move state up, replace with@Binding -
Mistake: Passing callbacks for direct value edits
Why it happens: trying to avoid bindings everywhere
How to fix it: use@Bindingfor direct data edits, callbacks for intent
When NOT to use this
If a value is purely local to one child and never needed outside, local @State is simpler than forwarding bindings across many layers.
Takeaway
If you remember one thing: forward bindings across layers, but keep ownership at the level that decides behavior.