How to pass data between views [SwiftUI]
There are several ways to pass information between views in SwiftUI, each suited for specific use cases and levels of data sharing.
@State and @Binding (Swift 5.1+/iOS 13+)
@State
is used for local state within a single view.@Binding
is for passing a reference to that state to a child view, allowing both views to read and write the value.
Use this when a parent owns the source of truth, and child views need to update it directly.
@State
is not designed for reference type (class). Changes to properties inside a class instance won’t trigger updates, because SwiftUI does not observe internal mutations unless a new instance is set. Instead, use@StateObject
for this purpose.
struct ParentView: View {
@State private var count: Int = 0
var body: some View {
ChildView(count: $count)
Text("Count: \(count)")
}
}
struct ChildView: View {
@Binding var count: Int
var body: some View {
Button("Increment") { count += 1 }
}
}
@StateObject and @ObservedObject (Swift 5.1+/iOS 13+)
@StateObject
is used when the view itself should own and create the observable object. It is used to own and instantiate an observable object within the view’s lifecycle, ensuring the object persists across view redraws.@ObservedObject
allows a view to observe an external, reference type (a class conforming to@ObservableObject
), and receive updates when it changes. They are ideal for view models or shared logic between several related views.
It’s not safe to use
@ObservedObject
if SwiftUI has the potential to create or redraw the screen. Unless you’re wrapping the@ObservedObject
object externally, using@StateObject
is a safe way to ensure the same result every time the screen is redrawn. Objects observed through@StateObject
are not destroyed even when the screen structure containing them is recreated.
@StateObject
triggers view updates on any changes to@Published
properties inside the observable object.
class CounterViewModel: ObservableObject {
@Published var count = 0
}
struct MainView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
ObserverView(viewModel: viewModel)
Text("Count: \(viewModel.count)")
}
}
struct ObserverView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
Button("Increment") { viewModel.count += 1 }
}
}
@Observable and @Bindable (Swift 5.9+/iOS 17+)
@Observable
and @Bindable
is a new macro in Swift’s Observation framework (iOS 17+), which greatly simplifies state management by reducing boilerplate and offering improved integration with SwiftUI. Unlike the protocol @ObservedObject
, all properties are automatically observed (published) and applied directly to a class as a macro.
@Observable class UserSettings {
var username: String = "Guest"
}
struct ContentView: View {
@Bindable var settings = UserSettings()
}
@EnvironmentObject (Swift 5.1+/iOS 13+)
@EnvironmentObject
injects a shared object into the environment, making it globally accessible to any child view that needs it. Use this for data that needs to be accessed by many views at different levels in the hierarchy, such as themes, user settings, or app-wide models.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
@main
struct MyApp: App {
@StateObject var settings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(settings)
}
}
}
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
Text("User: \(settings.username)")
EditView()
}
}
}
struct EditView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
TextField("Username", text: $settings.username)
}
}
Toggling with PreferenceKey (Swift 5.1+/iOS 13+)
PreferenceKey
allows passing information up the view hierarchy from child to parent, useful in cases like measuring view sizes or geometry that needs to be bubbled up. It is suitable for use cases such as modifying the view conforming to the user’s preference.
struct MyPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct ExampleView: View {
var body: some View {
VStack {
Color.red.frame(height: 100)
.background(GeometryReader {
Color.clear.preference(key: MyPreferenceKey.self, value: $0.size.height)
})
}
.onPreferenceChange(MyPreferenceKey.self) { value in
print("Height:", value)
}
}
}
@AppStorage and @SceneStorage (Swift 5.1+/iOS 14+)
For simplicity, lightweight @AppStorage
and @SceneStorage
provides a local database (key-value store) that saves view state per scene. It’s mainly used for sharing small pieces of data bound to user defaults or scene state, such as preferences and UI state, across different views and even app launches.
struct AppStorageExample: View {
@AppStorage("isDarkMode") var isDarkMode = false
var body: some View {
Toggle("Dark Mode", isOn: $isDarkMode)
}
}
struct SceneStorageExample: View {
@SceneStorage("selectedTab") var selectedTab: String?
var body: some View {
TabView(selection: $selectedTab) {
Text("First").tabItem { Text("First") }.tag("first")
Text("Second").tabItem { Text("Second") }.tag("second")
}
}
}