DEV Community

Vinayak G Hejib
Vinayak G Hejib

Posted on

The Art of Dependency Injection in SwiftUI

How I Stopped Worrying and Learned to Love Passing Stuff Around*

If SwiftUI had a motto, it might be: “Less is more, but good luck injecting that API client.”

In the world of SwiftUI, dependency injection is like dating: you want clarity, low maintenance, and definitely no surprises. Whether you're passing view models, shared state, or static configuration, how you inject those dependencies can make or break your architecture — and your sanity.

In this post, we’ll explore three elegant ways to inject dependencies into SwiftUI views and when to use (or avoid) each:

  • Constructor-based injection
  • @Environment-based injection with custom keys
  • @EnvironmentObject for shared observable state

We’ll use a fun example: a theme-aware counter screen. No analytics, no token managers — just a beautiful button and a splash of color.


1️⃣ Constructor-based Injection: The “Polite Guest” Approach

struct Theme { let background: Color let textColor: Color } struct CounterView: View { @State private var count = 0 let theme: Theme var body: some View { VStack { Text("Count: \(count)") .foregroundColor(theme.textColor) Button("Increment") { count += 1 } } .padding() .background(theme.background) .cornerRadius(12) } } 
Enter fullscreen mode Exit fullscreen mode

When to use it??:

  • You value clarity and control
  • You love previews and unit testing without wizardry
  • Your theme shouldn't mysteriously change mid-scroll

2️⃣ Environment Injection: The Magical Air You Breathe

Step 1: Define a Custom Environment Key

private struct ThemeKey: EnvironmentKey { static let defaultValue = Theme(background: .white, textColor: .black) } extension EnvironmentValues { var theme: Theme { get { self[ThemeKey.self] } set { self[ThemeKey.self] = newValue } } } 
Enter fullscreen mode Exit fullscreen mode

Step 2: Inject It Once, Use It Anywhere

struct CounterView: View { @Environment(\.theme) private var theme @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") .foregroundColor(theme.textColor) Button("Increment") { count += 1 } } .padding() .background(theme.background) .cornerRadius(12) } } 
Enter fullscreen mode Exit fullscreen mode

Step 3: Set the Theme from a Parent

struct RootView: View { var body: some View { CounterView() .environment(\.theme, Theme(background: .mint, textColor: .indigo)) } } 
Enter fullscreen mode Exit fullscreen mode

When to use it??:

  • Your dependency is lightweight and stable
  • You don't want to manually pass values 10 levels deep
  • You love magic that comes with a fallback value

3️⃣ @EnvironmentObject: The Loud Roommate

final class ThemeSettings: ObservableObject { @Published var theme = Theme(background: .white, textColor: .black) } struct CounterView: View { @EnvironmentObject var settings: ThemeSettings @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") .foregroundColor(settings.theme.textColor) Button("Increment") { count += 1 } } .padding() .background(settings.theme.background) .cornerRadius(12) } } 
Enter fullscreen mode Exit fullscreen mode

Inject it globally:

@main struct MyApp: App { @StateObject var themeSettings = ThemeSettings() var body: some Scene { WindowGroup { CounterView() .environmentObject(themeSettings) } } } 
Enter fullscreen mode Exit fullscreen mode

When to use it??:

  • You need observable shared state
  • You don’t mind runtime crashes when someone forgets .environmentObject(...)
  • You like living on the edge

⚖️ Environment Key vs EnvironmentObject: Explained with Snacks

You have a 🍪 (biscuit) Use @Environment Use @EnvironmentObject
The 🍪 never changes and can be safely shared
The 🍪 might change (someone might take a bite)
You want a default fallback 🍪
You forget to bring a 🍪 and the app crashes

Summary: Which Flavor to Choose?

Technique Best For Avoid If...
Constructor Injection Explicit, testable setup You hate writing initializers
@Environment + Key Global/static dependencies (themes) You need reactivity
@EnvironmentObject Shared state that updates views You need compile-time guarantees

Final Thought

SwiftUI gives you powerful, expressive tools for managing dependencies — just enough structure to keep your code clean, and just enough magic to feel ✨Swifty✨.

So the next time you’re passing a Theme, a Settings object, or even a metaphorical biscuit 🍪, ask yourself:

  • Does this need to be shared?
  • Does it change?
  • Do I want control or convenience?

Because dependency injection in SwiftUI is like parenting: it’s all about boundaries, visibility, and who gets to press the increment button. 😉

Top comments (1)

Collapse
 
nishit_goenka_156c71f13c1 profile image
Nishit Goenka

Nice way of explaining. Keep posting good content. Thanks