Why EnvironmentObject Can Be an Anti-Pattern
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.