Question 3
Dependency injection: initializers, containers, and test seams
The codebase uses a global ServiceLocator.shared singleton for networking and analytics. What problems does that cause, and how would you migrate toward something testable?
Follow-ups
- What about SwiftUI's environment vs manual injection?
Answer outline
A global ServiceLocator.shared hides what a type really needs. The code looks simple, but it secretly depends on networking, analytics, persistence, or feature flags. Four problems follow from that:
- 1.Hidden requirements: call sites cannot see what a type actually needs, so the initializer signature lies.
- 2.Test brittleness: tests must mutate shared global state, which leaks between tests and causes order-dependent failures.
- 3.Tight coupling: every caller is bound to one concrete implementation, with no seam where a test can slip in a fake.
- 4.Concurrency hazards: a mutable shared singleton invites data races under parallel tests or multiple scenes.
The better default is dependency injection: pass dependencies in through the initializer so the type's requirements are explicit and easy to swap.
Migrate gradually. Define protocols for the key services and inject them in new or touched code. Push the remaining service-locator calls up to the app's composition root, the one place that builds the object graph.
In SwiftUI, use the environment for dependencies shared by a whole view subtree. Prefer initializer injection when a dependency should be obvious at the call site and replaceable in tests.
Principles
- Never hide a dependency behind a global, because then the type's signature lies about what it needs.
- Prefer initializer injection so requirements are visible at the call site and easy to replace in tests.
- Introduce a protocol only when a test or a second implementation has to stand in for the real type.
- Push service lookup upward until only the composition root knows about concrete types.
- Treat the SwiftUI environment as scoped injection for a view subtree, never as a replacement singleton.
The initializer names both dependencies, so a test passes fakes and nothing reaches for a global:
final class ProfileViewModel {
private let profileAPI: ProfileAPI
private let analytics: AnalyticsTracking
init(profileAPI: ProfileAPI, analytics: AnalyticsTracking) {
self.profileAPI = profileAPI
self.analytics = analytics
}
}



