Why EnvironmentObject Can Be an Anti-Pattern

abdul ahad
3 min readFeb 27, 2025

--

Photo by Joshua Hanson on Unsplash

EnvironmentObject in SwiftUI can be considered an anti-pattern in certain cases due to its implicit dependency management and potential runtime crashes. However, it can also be useful when used correctly. Let’s break it down with examples.

Why @EnvironmentObject Can Be an Anti-Pattern (Ambient Context Issue)?

1. Implicit Dependencies (Hidden Coupling)

When using @EnvironmentObject, a child view expects that the parent has injected the required object. If the parent does not provide it, the app compiles fine but crashes at runtime.

Example: Unclear Dependency Injection

import SwiftUI

class AppViewModel: ObservableObject {
@Published var username: String = "A.Ahad"
}
struct ChildView: View {
@EnvironmentObject var viewModel: AppViewModel // Implicit dependency
var body: some View {
Text("User: \(viewModel.username)")
}
}

struct ParentView: View {
var body: some View {
ChildView() // No environment object provided!
}
}
struct ContentView: View {
var body: some View {
ParentView() // App crashes because ChildView expects AppViewModel
}
}

Issue

• ChildView assumes that AppViewModel will be available.

• ParentView does not inject the @EnvironmentObject, so the app crashes when accessing viewModel.

💡 Solution: Use explicit dependency injection (e.g., passing via initializer).

Fixed Version with Initializer Injection

struct ChildView: View {
let viewModel: AppViewModel // Explicit dependency
var body: some View {
Text("User: \(viewModel.username)")
}
}

struct ParentView: View {
let viewModel = AppViewModel()
var body: some View {
ChildView(viewModel: viewModel) // Now dependency is explicit
}
}

2. Harder to Test

If a view relies on @EnvironmentObject, writing unit tests becomes difficult because the dependency is not explicitly passed in.

Example of Test Problem

struct ProfileView: View {
@EnvironmentObject var viewModel: AppViewModel
var body: some View {
Text(viewModel.username)
}
}

• To test this view, we need to always provide an environment object in the test setup, which adds unnecessary complexity.

Better Approach for Testing

Instead of @EnvironmentObject, pass viewModel via an initializer:

struct ProfileView: View {
let viewModel: AppViewModel // Explicit dependency

var body: some View {
Text(viewModel.username)
}
}

• Now, we can easily create a test with a mock viewModel.

When EnvironmentObject is NOT an Anti-Pattern

While it has drawbacks, @EnvironmentObject is useful in some cases:

Global State (App-Level Shared Data)

If multiple views need global access to a shared object, EnvironmentObject is a good choice.

Example: Theme Settings

class ThemeSettings: ObservableObject {
@Published var isDarkMode = false
}

@main
struct MyApp: App {
let theme = ThemeSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(theme) // Injected once at the root
}
}
}
struct ContentView: View {
@EnvironmentObject var theme: ThemeSettings
var body: some View {
VStack {
Toggle("Dark Mode", isOn: $theme.isDarkMode)
}
}
}

Here, EnvironmentObject is fine because:

• The theme is truly global (used in multiple places).

• It is always provided at the app level, so there’s no risk of missing injection.

When to Use or Avoid EnvironmentObject

Final Thoughts

• @EnvironmentObject is convenient but risky.

• Use it only for truly global state (e.g., themes, global user session).

Avoid it for dependency injection, as it can introduce hidden dependencies, runtime crashes, and testing issues.

Prefer initializer injection to pass dependencies explicitly.

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet