How to use @EnvironmentObject in SwiftUI | SwiftUI Bootcamp #51
@ObservedObject requires you to explicitly pass a view model through every view in a chain — even views that don't use it. @EnvironmentObject solves this "prop drilling" problem by injecting a shared object into the environment once at the top level, making it available to any descendant view that declares it. After this lesson you'll understand when to use environment objects and how to avoid the runtime crash they cause when forgotten.
What You'll Learn
- How
.environmentObject()injects a shared object into a view's environment - How
@EnvironmentObjectretrieves that object in any descendant view without explicit passing - How to avoid the fatal "no EnvironmentObject found" crash
- The trade-offs between
@EnvironmentObjectand@ObservedObjectfor different app architectures
Mental Model
Imagine a hotel's piped water system. When the hotel manager turns on the water supply at the main valve (.environmentObject(viewModel)), every room in the hotel that has a faucet (@EnvironmentObject var viewModel) can access water without the manager having to run individual pipes to each room. A room in the middle of the building (like DetailView) doesn't need its own pipe — the water flows through the building's infrastructure automatically.
If a room tries to open the faucet before the water supply is connected (a view uses @EnvironmentObject without a parent calling .environmentObject()), it crashes — there's nothing in the pipe. This is why the runtime error message says "no EnvironmentObject found": the faucet is there, but no water was ever connected.
Detailed Explanation
@EnvironmentObject is SwiftUI's solution to sharing a single observable object across a deep view hierarchy without manually passing it through every level. You inject the object once using .environmentObject(myObject) on a view high in the hierarchy. Any descendant view that declares @EnvironmentObject var viewModel: MyViewModelType will automatically receive the same instance.
The injection is type-based. SwiftUI's environment looks up the object by its type (EnvironmentViewModel). If two different @EnvironmentObject declarations in the same hierarchy have the same type, they receive the same instance. If they have different types, each gets its own lookup. This means you can inject multiple different view models at once, each accessible by type.
The key difference from @ObservedObject is the propagation model. @ObservedObject requires every intermediate view in the chain to declare the property and pass it down explicitly — even if that view doesn't use it. @EnvironmentObject skips the middle layers entirely: DetailView does not declare the view model, yet FinalView (a descendant of DetailView) can access it freely.
When to use @EnvironmentObject: for truly global or near-global data that many views need — user authentication state, theme settings, shopping cart contents, app-wide notification counts. When NOT to use it: for data specific to one view or screen, or when the dependency graph should be explicit. Overusing environment objects makes the data flow invisible, which complicates testing and debugging.
Code Structure
EnvironmentObjectBootcamp.swift demonstrates a three-level navigation hierarchy. EnvironmentObjectBootcamp owns the view model with @StateObject and injects it with .environmentObject(viewModel). DetailView is a middle view that does NOT declare or use the view model — it simply displays a selected item and navigates forward. FinalView retrieves the view model with @EnvironmentObject and displays the full data array, demonstrating that it received the object without DetailView ever having to pass it.
Complete Code
EnvironmentObjectBootcamp.swift
import SwiftUI
// ObservedObject
// StateObject
// EnvironmentObject
class EnvironmentViewModel: ObservableObject {
@Published var dataArray: [String] = []
init() {
getData()
}
func getData() {
self.dataArray.append(contentsOf: ["iPhone", "iPad", "iMac", "Apple Watch"]) // loads sample data immediately
}
}
struct EnvironmentObjectBootcamp: View {
@StateObject var viewModel: EnvironmentViewModel = EnvironmentViewModel() // creates and owns the view model
var body: some View {
NavigationView {
List {
ForEach(viewModel.dataArray, id: \.self) { item in
NavigationLink(
destination: DetailView(selectedItem: item),
label: {
Text(item)
})
}
}
.navigationTitle("iOS Devices")
}
.environmentObject(viewModel) // injects viewModel into the environment for ALL descendants in this hierarchy
}
}
struct DetailView: View {
let selectedItem: String // receives only what it needs: the selected item string
// Note: no @EnvironmentObject declaration here — DetailView does not need the view model
var body: some View {
ZStack {
// background
Color.orange.ignoresSafeArea()
//foreground
NavigationLink(
destination: FinalView(), // FinalView will retrieve the EnvironmentObject on its own
label: {
Text(selectedItem)
.font(.headline)
.foregroundColor(.orange)
.padding()
.padding(.horizontal)
.background(Color.white)
.cornerRadius(30)
})
}
}
}
struct FinalView: View {
@EnvironmentObject var viewModel: EnvironmentViewModel // retrieves the injected object by type — no passing required
var body: some View {
ZStack {
// background
LinearGradient(
gradient: Gradient(colors: [Color(#colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1)), Color(#colorLiteral(red: 0.09019608051, green: 0, blue: 0.3019607961, alpha: 1))]),
startPoint: .topLeading,
endPoint: .bottomTrailing)
.ignoresSafeArea()
// foreground
ScrollView {
VStack(spacing: 20) {
ForEach(viewModel.dataArray, id: \.self) { item in // reads the full array from the shared view model
Text(item)
}
}
.foregroundColor(.white)
.font(.largeTitle)
}
}
}
}
struct EnvironmentObjectBootcamp_Previews: PreviewProvider {
static var previews: some View {
EnvironmentObjectBootcamp()
//DetailView(selectedItem: "iPhone")
//FinalView() // ← this would crash in preview: no EnvironmentObject injected
}
}Code Walkthrough
class EnvironmentViewModel: ObservableObject— The shared observable class.@Published var dataArrayis the data that all views in the hierarchy can read and respond to.getData()is called ininit()so data is ready immediately.@StateObject var viewModel: EnvironmentViewModel = EnvironmentViewModel()—EnvironmentObjectBootcampis the owner.@StateObjectensures the object is created once and lives as long as this view. This is where the view model lifecycle is anchored..environmentObject(viewModel)— Injects the view model into the SwiftUI environment. This modifier must be placed on a view that is an ancestor of every view that needs to use@EnvironmentObject. Placing it on theNavigationViewmeans all views pushed onto the navigation stack are descendants and can access it.DetailViewwith NO@EnvironmentObject— This is the key demonstration.DetailViewsits betweenEnvironmentObjectBootcampandFinalViewin the navigation hierarchy. It does not declare or useEnvironmentViewModelat all. Yet the environment object flows through it automatically toFinalView.@EnvironmentObject var viewModel: EnvironmentViewModelinFinalView— This declaration tells SwiftUI to look up anEnvironmentViewModelinstance in the environment. SwiftUI finds it becauseEnvironmentObjectBootcampinjected it up the hierarchy. No explicit passing fromDetailViewwas needed.Commented-out
FinalView()in previews — Illustrates the runtime crash risk. If you previewFinalViewdirectly without injecting the environment object, you get a crash: "No ObservableObject of type EnvironmentViewModel found." The fix is to add.environmentObject(EnvironmentViewModel())to the preview.ForEach(viewModel.dataArray, id: \.self)inFinalView— Even thoughFinalViewnever received the view model explicitly, it readsviewModel.dataArraythrough the environment. Changes to the array inEnvironmentObjectBootcamp(e.g., adding or removing items) would automatically updateFinalViewtoo.
Common Mistakes
Mistake: Forgetting .environmentObject() on the parent, causing a "no EnvironmentObject found" crash at runtime
This crash happens at runtime, not at compile time — the app builds and launches, then crashes when the view with @EnvironmentObject appears. Always inject the object at or above the level of the first view that uses @EnvironmentObject. For Xcode Previews, add .environmentObject(MyViewModel()) to the preview's return value.
Mistake: Using @EnvironmentObject everywhere, even for data that only one or two views need@EnvironmentObject hides the data flow — you can't see at a glance which views depend on which objects. Use it only for truly shared, app-wide data (authentication, theme, global session). For data used by just one view tree, @StateObject + @ObservedObject makes the dependency graph explicit and testable.
Mistake: Previewing a view with @EnvironmentObject without providing the object in the preview
Xcode Previews have their own environment separate from the app's environment. A view using @EnvironmentObject in a preview must have the object injected in PreviewProvider. The pattern is: FinalView().environmentObject(EnvironmentViewModel()). Forgetting this causes the preview to crash with a "Fatal error" rather than rendering.
Key Takeaways
.environmentObject(viewModel)injects a shared object into the SwiftUI environment; any descendant that declares@EnvironmentObjectreceives the same instance automatically.- Intermediate views in the hierarchy do NOT need to pass the object — they are transparent to it, which eliminates prop-drilling through layers that don't use the data.
- Always inject the environment object in
PreviewProviderand at the correct ancestor level in your app; missing the injection causes a fatal runtime crash, not a compile-time error.
Last updated: June 27, 2026