Skip to content

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 @EnvironmentObject retrieves that object in any descendant view without explicit passing
  • How to avoid the fatal "no EnvironmentObject found" crash
  • The trade-offs between @EnvironmentObject and @ObservedObject for 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

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

  1. class EnvironmentViewModel: ObservableObject — The shared observable class. @Published var dataArray is the data that all views in the hierarchy can read and respond to. getData() is called in init() so data is ready immediately.

  2. @StateObject var viewModel: EnvironmentViewModel = EnvironmentViewModel()EnvironmentObjectBootcamp is the owner. @StateObject ensures the object is created once and lives as long as this view. This is where the view model lifecycle is anchored.

  3. .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 the NavigationView means all views pushed onto the navigation stack are descendants and can access it.

  4. DetailView with NO @EnvironmentObject — This is the key demonstration. DetailView sits between EnvironmentObjectBootcamp and FinalView in the navigation hierarchy. It does not declare or use EnvironmentViewModel at all. Yet the environment object flows through it automatically to FinalView.

  5. @EnvironmentObject var viewModel: EnvironmentViewModel in FinalView — This declaration tells SwiftUI to look up an EnvironmentViewModel instance in the environment. SwiftUI finds it because EnvironmentObjectBootcamp injected it up the hierarchy. No explicit passing from DetailView was needed.

  6. Commented-out FinalView() in previews — Illustrates the runtime crash risk. If you preview FinalView directly 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.

  7. ForEach(viewModel.dataArray, id: \.self) in FinalView — Even though FinalView never received the view model explicitly, it reads viewModel.dataArray through the environment. Changes to the array in EnvironmentObjectBootcamp (e.g., adding or removing items) would automatically update FinalView too.

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 @EnvironmentObject receives 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 PreviewProvider and 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

Released under the MIT License.