Skill: Dependency Injection
Overview
Dependency Injection (DI) provides a type's collaborators from the outside (via initializers or properties) instead of constructing them internally. It is the mechanism that makes Clean Architecture testable: by depending on protocols and injecting concrete implementations at a composition root, you can swap real services for test doubles. Prefer constructor injection; reserve @Environment for SwiftUI-scoped values.
Use Cases
- Making ViewModels/use cases/repositories unit-testable with stubs.
- Swapping implementations per environment (prod vs mock vs preview).
- Breaking hidden coupling to singletons.
Best Practices
- Constructor injection by default. Dependencies are explicit init parameters.
- Depend on protocols, not concrete types.
- Wire everything in a single composition root (e.g. an app-level factory/container).
- Use SwiftUI
@Environment/EnvironmentObjectonly for genuinely view-scoped dependencies. - Provide preview/test factories that inject fakes.
Anti-Patterns
- ❌ Service Locator / global singletons accessed deep inside types (hidden dependencies).
- ❌ Constructing
URLSession/repositories inside a ViewModel. - ❌ Heavyweight DI frameworks for a small app when initializers suffice.
- ❌ Optional injected dependencies with
nildefaults that hide wiring mistakes.
Checklist
- [ ] Dependencies are injected via init, not constructed internally.
- [ ] Types depend on protocols, not concretes.
- [ ] A single composition root wires the graph.
- [ ] Test/preview factories provide fakes.
- [ ] No global singletons reached from business logic.
Swift Examples
swift
// Manual constructor injection + a small composition root
protocol AuthService { func login(_ email: String, _ password: String) async throws -> Session }
@MainActor @Observable
final class LoginViewModel {
private let auth: AuthService
init(auth: AuthService) { self.auth = auth } // injected
}
enum AppContainer { // composition root
static func makeLoginViewModel() -> LoginViewModel {
LoginViewModel(auth: LiveAuthService(client: .live))
}
#if DEBUG
static func previewLoginViewModel() -> LoginViewModel {
LoginViewModel(auth: StubAuthService())
}
#endif
}swift
// SwiftUI environment injection for view-scoped values
private struct ThemeKey: EnvironmentKey { static let defaultValue = Theme.system }
extension EnvironmentValues { var theme: Theme {
get { self[ThemeKey.self] } set { self[ThemeKey.self] = newValue } } }Common Interview Questions
- Constructor vs property vs method injection — when to use each?
- Why is Service Locator considered an anti-pattern?
- How does DI enable unit testing?
- What is a composition root?
- When (if ever) is a DI framework worth it on iOS?
AI Implementation Notes
- Default to constructor injection with protocols; avoid singletons in generated code.
- Generate a composition root and a
#if DEBUGpreview/test factory with stubs. - When generating a ViewModel, never
newup its dependencies inside it. - Related:
mvvm.md,clean_architecture.md,repository_pattern.md.