Dependency injection and the composition root
Dependency injection means a type receives the collaborators it needs. A composition root is the narrow startup location that chooses concrete live, preview, or test implementations and connects them.
Make the graph visible
struct AppDependencies { var clock: @Sendable () -> Date var makeID: @Sendable () -> UUID var library: any NoteLibrary var analytics: any Analytics}
@MainActorfunc makeApp(dependencies: AppDependencies) -> AppModel { let createNote = CreateNote( makeID: dependencies.makeID, now: dependencies.clock, save: dependencies.library.save ) return AppModel(createNote: createNote, analytics: dependencies.analytics)}Production startup supplies a durable library and real clock. Tests supply fixed time, predictable IDs, an in-memory library, and recording analytics. The use case itself does not branch on environment.
Prefer explicit construction
Initializer injection makes required collaborators visible and keeps instances immutable. Method injection suits a value needed for one operation. Environment-based injection can be useful near SwiftUI, but core dependencies should still have clear ownership and defaults that do not silently reach production services.
Avoid mutable service location
A singleton registry hides the dependency graph, permits tests to affect one another, and allows any code to acquire any service. Moving a global behind a protocol changes its spelling, not its lifetime or ownership risk.
The composition root may be framework-specific because startup is framework-specific. The objects it creates can remain independent.
Series navigation
- Previous: Part 63: Domain models, value objects, invariants, and use cases
- Next: Part 65: Coordinators, routers, deep links, and restoration
- Series index: Zero to iOS Hero
References
- Swift packages describes package and module composition boundaries.
- SwiftUI model data covers dependency and observable data flow in SwiftUI.