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