SwiftUI data flow works best when each piece of state has one clear owner. A parent view or model owns the source of truth, and other views either read that value, mutate it through a binding, or report derived values back up the view tree.
For modern SwiftUI apps, the common patterns are:
Older apps that use ObservableObject still commonly use @StateObject, @ObservedObject, and @EnvironmentObject. Those wrappers are still valid for Combine-based models, but new code can often use the Observation framework instead.
The simplest way to pass data down is with a normal stored property. Use this when the child view only needs to display a value.
struct ProfileView: View {
let username: String
var body: some View {
VStack {
Text("Signed in as")
AccountBadge(username: username)
}
}
}
struct AccountBadge: View {
let username: String
var body: some View {
Text(username)
.font(.headline)
}
}This is the default option for one-way data flow. You do not need a property wrapper unless the child needs to mutate the value or observe a shared model.
Use @Binding when a child view needs to edit state owned by its parent. The parent stores the value with @State, and passes a binding with the $ prefix.
struct ContentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "Switch is on" : "Switch is off")
Toggle("Toggle switch", isOn: $isOn)
ChildView(isOn: $isOn)
}
}
}
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
VStack {
Text(isOn ? "Child switch is on" : "Child switch is off")
Button("Toggle child switch") {
isOn.toggle()
}
}
}
}@Binding does not own the data. It only gives the child a read-write connection to state stored somewhere else.
For shared mutable model data on iOS 17, macOS 14, and later, define the model with @Observable. A parent can own the model with @State, pass it to child views as a regular property, and use @Bindable when a child needs bindings to the model’s mutable properties.
import Observation
import SwiftUI
@Observable
class Account {
var name = "byby"
var isPro = false
}
struct AccountView: View {
@State private var account = Account()
var body: some View {
Form {
AccountSummary(account: account)
AccountEditor(account: account)
}
}
}
struct AccountSummary: View {
let account: Account
var body: some View {
Text(account.isPro ? "\(account.name) Pro" : account.name)
}
}
struct AccountEditor: View {
@Bindable var account: Account
var body: some View {
TextField("Name", text: $account.name)
Toggle("Pro account", isOn: $account.isPro)
}
}For ObservableObject models, the older equivalent is to own the object with @StateObject, pass it down with @ObservedObject, or inject it broadly with @EnvironmentObject.
Use the environment for values that many descendants need, such as account context, feature flags, formatters, or app-wide settings. Avoid using it for every small value, because explicit properties make dependencies easier to see.
With recent SwiftUI SDKs, the concise way to create a custom environment value is to extend EnvironmentValues with @Entry.
import SwiftUI
struct AccountContext {
var name: String
var token: String?
}
extension EnvironmentValues {
@Entry var currentAccount = AccountContext(name: "Anonymous")
}
struct ContentView: View {
@State private var currentAccount = AccountContext(name: "byby", token: "foo")
var body: some View {
ProfileButton()
.environment(\.currentAccount, currentAccount)
}
}
struct ProfileButton: View {
@Environment(\.currentAccount) private var currentAccount
var body: some View {
Button("Show \(currentAccount.name)") {
print("Token: \(currentAccount.token ?? "none")")
}
}
}If you need to support older SwiftUI toolchains, define a custom EnvironmentKey type and add a computed property to EnvironmentValues instead.
You can also provide a dedicated modifier to make call sites read better.
extension View {
func currentAccount(_ account: AccountContext) -> some View {
environment(\.currentAccount, account)
}
}Preferences allow child views to report values up to an ancestor. They are best for values discovered while building or laying out views, such as a child’s size, anchor, scroll offset, or selected tab metadata.
Do not use preferences as a replacement for normal actions or bindings. If a child needs to request an action, pass a closure. If it needs to edit parent-owned state, use @Binding.
struct ChildWidthKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct ParentView: View {
@State private var widestChild: CGFloat = 0
var body: some View {
VStack(alignment: .leading) {
Text("Widest child: \(Int(widestChild))")
ChildView(title: "Short")
ChildView(title: "A much longer title")
}
.onPreferenceChange(ChildWidthKey.self) { width in
widestChild = width
}
}
}
struct ChildView: View {
let title: String
var body: some View {
Text(title)
.background {
GeometryReader { proxy in
Color.clear
.preference(key: ChildWidthKey.self, value: proxy.size.width)
}
}
}
}The child writes a preference, and the parent observes the combined value with onPreferenceChange. The reduce(value:nextValue:) method decides how to combine values when multiple children report the same preference.