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)
}
}
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 }
}
}
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)
}
}
Step 3: Set the Theme from a Parent
struct RootView: View {
var body: some View {
CounterView()
.environment(\.theme, Theme(background: .mint, textColor: .indigo))
}
}
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)
}
}
Inject it globally:
@main
struct MyApp: App {
@StateObject var themeSettings = ThemeSettings()
var body: some Scene {
WindowGroup {
CounterView()
.environmentObject(themeSettings)
}
}
}
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)
Nice way of explaining. Keep posting good content. Thanks