How do you decide between @State, @StateObject, @ObservedObject, and @EnvironmentObject?

Map SwiftUI property wrappers to ownership boundaries rather than memorizing them in isolation

Answer

Ownership is the center of the answer

SwiftUI views are value types that can be recreated frequently, so mutable state cannot just live in the view struct itself. These property wrappers let SwiftUI keep the underlying state outside the transient view value while still updating the UI when the data changes.

If the view owns the lifetime of a reference type, @StateObject is usually the right choice. If the view receives an already-owned object from elsewhere, @ObservedObject is typically the better fit.

Quick mapping

  • @State for simple local value state.
  • @StateObject for reference-type state owned by the view.
  • @ObservedObject for externally owned observable state.
  • @EnvironmentObject for shared dependencies injected high in the tree.

Use cases and examples

Use @State for local UI values such as isExpanded, the current tab, a search query, or the text a user is typing into a form field.

swift
struct SettingsSheet: View {
    @State private var isSaving = false
    @State private var username = ""

    var body: some View {
        Form {
            TextField("Username", text: $username)
            Button("Save") {
                isSaving = true
            }
        }
    }
}

@State fits here because isSaving and username are local value state owned entirely by this view.

Use @StateObject when a view creates and owns a feature-level model, such as ProfileScreen creating its own ProfileViewModel that loads data and survives body recomputation.

swift
struct ProfileScreen: View {
    @StateObject private var viewModel = ProfileViewModel()

    var body: some View {
        Text(viewModel.displayName)
    }
}

@StateObject is correct when the screen creates the view model and should keep it alive across normal view redraws. You would not use @State here because @State is for local value state such as Bool, String, or small structs. A view model is a reference type conforming to ObservableObject, so the view needs an ownership wrapper that preserves that object identity and reacts to its published changes.

Use @ObservedObject when a parent or coordinator already owns the model and passes it down, such as ProfileScreen(viewModel: viewModel) or a row view observing a shared item model.

swift
struct ProfileContainer: View {
    @StateObject private var viewModel = ProfileViewModel()

    var body: some View {
        ProfileScreen(viewModel: viewModel)
    }
}

struct ProfileScreen: View {
    @ObservedObject var viewModel: ProfileViewModel

    var body: some View {
        Text(viewModel.displayName)
    }
}

@ObservedObject is correct in the child because the parent already owns the object. The child should react to updates, not control the lifetime. In other words, @ObservedObject is a subscription wrapper, not an ownership wrapper.

Use @EnvironmentObject for app-wide or subtree-wide shared state, such as an authenticated SessionStore, theme settings, or navigation-level user preferences that many descendants read.

swift
final class SessionStore: ObservableObject {
    @Published var currentUserName: String?
}

@main
struct InterviewPrepApp: App {
    @StateObject private var session = SessionStore()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environmentObject(session)
        }
    }
}

struct GreetingView: View {
    @EnvironmentObject var session: SessionStore

    var body: some View {
        Text(session.currentUserName ?? "Guest")
    }
}

@EnvironmentObject works when many descendants need the same shared dependency and passing it through every initializer would just be plumbing. It is still not owning the object locally; an ancestor must create and inject it, and this view simply reads it implicitly from the environment.

Interview nuance

The strongest answers mention view identity. If a view recreates often, using the wrong wrapper can accidentally recreate the object and reset the feature state. A common mistake is creating a view model inside a view and marking it @ObservedObject; that usually means @StateObject was the right choice. Another good distinction is explicit versus implicit dependency flow: prefer @ObservedObject when you want the dependency visible in the initializer, and prefer @EnvironmentObject when many descendants truly share the same object and explicit threading would add noise.