@EnvironmentObject & the Environment
Reviewed & published by Brayan K
Passing a shared model through every view gets tedious fast. SwiftUI's environment lets you inject an object once near the top and read it anywhere below with @EnvironmentObject. You'll also meet @Environment for built-in values like dark mode and the dismiss action.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Building iOS Apps with SwiftUI • Intermediate
What You'll Learn in This Lesson
- • Inject a shared object with .environmentObject(_)
- • Read it deep in the tree with @EnvironmentObject
- • Avoid the "not injected" runtime crash
- • Read built-in values with @Environment
- • Use colorScheme and dismiss
1️⃣ Injecting with .environmentObject(_)
Create your shared model (an ObservableObject) and own it with @StateObject. Then attach .environmentObject(_) to an ancestor view. Everything below it can now reach that instance.
import SwiftUI
// A shared, app-wide model.
class Session: ObservableObject {
@Published var username = "Guest"
@Published var isLoggedIn = false
}
// Inject the object into the ENVIRONMENT with .environmentObject(_).
// Every descendant view below can now read it — no manual passing.
struct RootView: View {
@StateObject private var session = Session()
var body: some View {
HomeView()
.environmentObject(session) // place 'session' in the environment
}
}2️⃣ Reading with @EnvironmentObject
A descendant declares @EnvironmentObject var session: Session and SwiftUI supplies the injected instance — even if many views sit between them. Intermediate views never have to forward it.
import SwiftUI
// A DEEP descendant reads the injected object directly with
// @EnvironmentObject — even if many views sit in between.
struct ProfileBadge: View {
@EnvironmentObject var session: Session // supplied by an ancestor
var body: some View {
Text("Signed in as \(session.username)")
}
}
// HomeView never had to receive or forward 'session' — ProfileBadge just
// asks the environment for it.
struct HomeView: View {
var body: some View {
VStack {
Text("Home")
ProfileBadge()
}
}
}3️⃣ @Environment — Built-In Values
@Environment(\.key) reads built-in keyed values rather than your own objects: the current colorScheme (light/dark), the locale, the dismiss action, and more.
import SwiftUI
// @Environment reads BUILT-IN keyed values, not your own objects.
struct DetailView: View {
// light or dark mode
@Environment(\.colorScheme) private var colorScheme
// an action you call to close this screen
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
Text(colorScheme == .dark ? "Dark mode" : "Light mode")
Button("Close") {
dismiss() // closes the presented view
}
}
}
}Your turn. Fill in the blanks to inject an object and read it in a descendant.
// 🎯 YOUR TURN — fill in each ___ then preview in Xcode.
import SwiftUI
class AppState: ObservableObject {
@Published var theme = "system"
}
struct AppRoot: View {
@StateObject private var state = AppState()
var body: some View {
SettingsScreen()
.___(state) // 👉 modifier that injects into the environment
}
}
struct SettingsScreen: View {
// Read the injected AppState from the environment.
___ var state: AppState // 👉 the wrapper for an injected object
var body: some View {
Text("Theme: \(state.theme)")
}
}Pro Tips
- 💡 Inject near the root. One .environmentObject high up covers the whole tree.
- 💡 Inject in previews too. A preview that reads an environment object must also inject it, or it crashes.
- 💡 Use it for app-wide state. For a value just one child needs, prefer @Binding or a plain property.
Common Errors (and the fix)
- "No ObservableObject of type Session found" — you forgot .environmentObject(session) on an ancestor. Inject it before any descendant reads it.
- Preview crashes immediately — add .environmentObject(...) in the #Preview too.
- Reading your own object with @Environment — use @EnvironmentObject for injected objects; @Environment is for built-in keyed values.
Mini-Challenge: App-Wide Theme
Create a Theme: ObservableObject with a @Published var isDark = false. Inject it at the root, then build two unrelated child views several levels deep: one with a toggle button that flips isDark, and another that displays the current theme. Confirm both react without passing the object through any intermediate view.
🎉 Lesson Complete!
- ✅ Inject a shared object once with .environmentObject(_)
- ✅ Read it anywhere below with @EnvironmentObject
- ✅ Forgetting to inject = a runtime crash — inject near the root
- ✅ @Environment(\.key) reads built-in values like colorScheme and dismiss
Practice quiz
What problem does @EnvironmentObject solve?
- Storing data on disk
- Passing a shared object deep down without threading it through every view
- Drawing animations
- Networking
Answer: Passing a shared object deep down without threading it through every view. @EnvironmentObject injects a shared model into the environment so deep descendants read it directly.
How do you inject an object into the environment?
- With the .environmentObject(_) modifier on a parent view
- By writing @State
- With a global variable
- It is automatic for all objects
Answer: With the .environmentObject(_) modifier on a parent view. Call .environmentObject(myObject) on an ancestor view to place it in the environment for descendants.
How does a descendant view read an injected object?
- @State var model
- @Binding var model
- @EnvironmentObject var model: MyModel
- import model
Answer: @EnvironmentObject var model: MyModel. Declare @EnvironmentObject var model: MyModel and SwiftUI supplies the injected instance.
What must the injected type conform to (pre-iOS 17)?
- View
- ObservableObject
- Codable
- Hashable
Answer: ObservableObject. An @EnvironmentObject is an ObservableObject, so its @Published changes refresh observing views.
What happens if you read an @EnvironmentObject that was never injected?
- It returns nil
- The app crashes at runtime
- It returns a default
- It compiles to an empty view
Answer: The app crashes at runtime. Forgetting to inject the object crashes at runtime because there is nothing to supply.
What does @Environment(\.colorScheme) read?
- A custom model
- A built-in environment value (light or dark mode)
- The view's frame
- User defaults
Answer: A built-in environment value (light or dark mode). @Environment reads built-in environment values like colorScheme, locale, or dismiss.
What is the difference between @EnvironmentObject and @Environment?
- They are identical
- @EnvironmentObject reads an injected ObservableObject; @Environment reads keyed environment values
- @Environment is for networking
- @EnvironmentObject is deprecated
Answer: @EnvironmentObject reads an injected ObservableObject; @Environment reads keyed environment values. @EnvironmentObject pulls a shared object; @Environment(\.key) reads a specific keyed value.
Which value would you read with @Environment(\.dismiss)?
- A color
- An action to close the current screen
- The device name
- A binding to text
Answer: An action to close the current screen. @Environment(\.dismiss) gives a dismiss action you call to close a presented view.
Why is @EnvironmentObject convenient for app-wide state?
- It is faster than a database
- Any descendant can read it without manual passing through intermediaries
- It encrypts data
- It avoids using classes
Answer: Any descendant can read it without manual passing through intermediaries. It removes the need to pass the object through every intermediate view's initializer.
Where do you typically inject app-wide objects?
- In a leaf view
- Inside a Button
- In a Text view
- Near the root, e.g. on the top-level view in the App
Answer: Near the root, e.g. on the top-level view in the App. Inject near the root so the whole view tree below it can access the shared object.
Continue this course
- Previous: @StateObject & ObservableObject
- Next: Layout: Stacks, Frames & GeometryReader — VStack/HStack/ZStack, alignment, frames and GeometryReader
Frequently asked questions
What is @EnvironmentObject?
@EnvironmentObject is a property wrapper that reads a shared ObservableObject from SwiftUI's environment. Instead of passing a model through every view's initializer, an ancestor injects it once with .environmentObject(_), and any descendant — however deep — declares @EnvironmentObject to read it. It's ideal for app-wide state like a session or settings.
How do I inject an object into the environment?
Call the .environmentObject(myObject) modifier on an ancestor view, typically near the root of your app. That places the instance in the environment for the entire subtree below it. Any descendant that declares @EnvironmentObject of the matching type then receives that same instance automatically.
What happens if I forget to inject the object?
Your app crashes at runtime when a view tries to read an @EnvironmentObject that was never injected — there's no instance to supply. Always make sure an ancestor calls .environmentObject(_) for every environment object type your descendants read, including in previews.
How is @Environment different from @EnvironmentObject?
@EnvironmentObject reads a shared ObservableObject you injected. @Environment(\.someKey) reads a built-in, keyed environment value such as colorScheme (light/dark), locale, or the dismiss action. One pulls your own shared model; the other reads system-provided values keyed by a key path.
Where should I inject app-wide objects?
Inject them high in the tree — usually on the top-level view returned by your App's body — so the whole view hierarchy below can read them. Injecting near the root means every screen and subview shares the same instance without any manual threading through intermediate views.