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
@Statefor simple local value state.@StateObjectfor reference-type state owned by the view.@ObservedObjectfor externally owned observable state.@EnvironmentObjectfor 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.
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.
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.
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.
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.