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