How to Pass Data Up & Down in SwiftUI

Updated May 16, 2026#how-to#swiftui

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:

  • Use @State for local view state and to own @Observable model objects.
  • Use regular properties to pass read-only values down to child views.
  • Use @Binding to let a child edit state owned by its parent.
  • Use @Environment or custom environment values for values many descendants need.
  • Use PreferenceKey when a child view needs to report layout or other derived values up to an ancestor.

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.

Pass read-only data down

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.

Pass editable data with @Binding

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.

Pass model data with Observation

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.

Pass data down using environment values

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)
  }
}

Pass data up using preferences

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.