Passing a value down through every view in a hierarchy, layer by layer, gets unwieldy fast when many unrelated views need the same piece of shared state — a logged-in user, a theme setting, a shared data store. SwiftUI’s environment system solves that, and which wrapper you use for it depends on which SwiftUI version you’re targeting.
@EnvironmentObject: The Original Mechanism
A parent injects an ObservableObject into the environment once, and any descendant view — no matter how deeply nested — can read it without it being passed explicitly through every view in between.
class Session: ObservableObject {
@Published var user: User?
}
// Inject it once, high in the hierarchy
ContentView()
.environmentObject(Session())
// Read it anywhere below, with no explicit passing
struct ProfileView: View {
@EnvironmentObject var session: Session
var body: some View {
Text(session.user?.name ?? "Not logged in")
}
}
The tradeoff: if a view declares @EnvironmentObject var session: Session but nothing upstream actually injected a Session, the app crashes at runtime. The compiler can’t catch a missing .environmentObject() call.
@Environment: The Broader, Newer Mechanism
@Environment predates @EnvironmentObject for reading system values (like colorScheme or dismiss), but as of iOS 17, it also handles custom @Observable models — replacing @EnvironmentObject for that use case entirely:
@Observable
class Session {
var user: User?
}
ContentView()
.environment(Session())
struct ProfileView: View {
@Environment(Session.self) private var session
var body: some View {
Text(session.user?.name ?? "Not logged in")
}
}
System Values Still Use @Environment
Reading framework-provided values like the current color scheme or a dismiss action has always used @Environment with a key path, and that doesn’t change:
@Environment(.colorScheme) var colorScheme
@Environment(.dismiss) var dismiss
Which One to Use
If your minimum deployment target is iOS 17+, use @Observable models with .environment() / @Environment(Type.self) — it’s the direction Apple is taking the framework, and it plays more cleanly with the rest of the @Observable migration. If you still support iOS 16 or earlier, ObservableObject with @EnvironmentObject remains the only option, and there’s nothing wrong with continuing to use it until you can drop that support.