@State vs @Binding vs @ObservedObject vs @StateObject vs @EnvironmentObject — what's the actual difference?
Five property wrappers that all say 'this view depends on some state,' but differ in who owns the storage, when it's created, and how far a change propagates — mixing them up is a very common SwiftUI interview stumble.
LEARN BY DOINGPut your intuition to work3 steps
@State vs @Binding vs @ObservedObject vs @StateObject vs @EnvironmentObject — what's the actual difference?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.All five of these look nearly identical at the call site — @Something var x — which is exactly why they make a good interview discriminator: the differences are not about what kind of data each one holds, they are about two orthogonal questions. Who owns the storage, and when is it created? And how far does a write propagate? Answer those two questions for each wrapper and the whole table falls out on its own.
@State is for a value type owned by this view. The syntax looks like it stores the value inside the view struct, but it does not — SwiftUI allocates persistent storage for it outside the struct entirely, keyed to the view's identity, precisely because the view struct itself gets thrown away and recreated on every single body evaluation of its parent. @State's storage is what survives that churn. It should almost always be private, because nothing outside this view should touch it directly — if a child needs to read or write it, you hand down a Binding, not the raw value.
@Binding is not storage at all. It is a get/set reference into state owned somewhere else — usually a parent's @State, or an object's @Published property, exposed through the $ prefix. A child declaring @Binding var isOn: Bool can read and write it exactly as if it were a local @State, but every write travels back up to the real source of truth, and that write is what actually triggers a re-render — the binding itself holds nothing.
@ObservedObject is for reference types conforming to ObservableObject, and the critical fact is that the view using it does not own the object's lifetime — it only observes an instance that already exists, typically handed in from a parent. This is where the most common real bug in production SwiftUI code lives: write @ObservedObject var vm = ViewModel() with the initializer inline, and that initializer expression runs on every init of the containing view struct — which means every time the parent re-renders and recreates this child view's value, you silently get a brand-new ViewModel, and every piece of state it was holding is gone.
@StateObject exists specifically to close that hole. Same ObservableObject requirement, but SwiftUI guarantees the initial-value expression runs exactly once per view identity, and the resulting instance survives across however many times the parent re-renders. The rule of thumb that resolves almost every mix-up: if this view creates the object, it is @StateObject; if it merely receives an object someone else already created, it is @ObservedObject.
@EnvironmentObject is an ObservableObject injected into the environment by some ancestor via .environmentObject(_:), implicitly available to any descendant that declares @EnvironmentObject var x: T — no threading it through every intermediate initializer. The trade is real: convenient for something genuinely app-wide (an auth session, a theme, a cart), but the dependency becomes invisible at the call site, and a missing .environmentObject(_:) upstream is a runtime crash, not a compile-time error.
struct ProfileScreen: View {@State private var isEditing = false// Owns this object: created here, must survive this view's re-renders.@StateObject private var viewModel = ProfileViewModel()// Injected by an ancestor via .environmentObject(_:) somewhere above.@EnvironmentObject var session: SessionManagervar displayName: String {session.currentUser?.name ?? "Guest"}var body: some View {VStack {Text("Signed in as \(displayName)")// $isEditing turns the @State into a Binding<Bool> for the child.EditToggleRow(isEditing: $isEditing)// Passed down, not created here — the child only observes it.ProfileDetailView(viewModel: viewModel)}}}struct EditToggleRow: View {@Binding var isEditing: Boolvar body: some View {Toggle("Edit mode", isOn: $isEditing)}}struct ProfileDetailView: View {// Does NOT own the object's lifetime — just observes one handed to it.@ObservedObject var viewModel: ProfileViewModelvar body: some View {TextField("Display name", text: $viewModel.displayName)}}final class ProfileViewModel: ObservableObject {@Published var displayName: String = ""}final class SessionManager: ObservableObject {@Published var currentUser: User?}struct User {let name: String}
It's worth closing with where this is heading: iOS 17's @Observable macro plus @Bindable collapses part of this table. Plain classes marked @Observable no longer need @Published at all, a view that owns one just uses @State instead of @StateObject (the ownership rule stays identical, only the wrapper name changes for reference types), and one that receives one uses a plain property instead of @ObservedObject. The five-wrapper question above is still asked because most production codebases are years away from finishing that migration — but naming the direction things are moving is exactly the kind of detail that separates "I memorized this" from "I use this."
🧠 Check your understanding
0/3 · 0/3 answered1. A view declares @ObservedObject var vm = ProfileViewModel() directly, with no value passed in from a parent. What is the risk?
2. Which of these is not storage at all, but a two-way reference to state owned somewhere else?
3. Why might a team prefer @EnvironmentObject for a SessionManager shared across a dozen unrelated screens?