How do you do dependency injection in Swift, and when do you need a container or framework?

15.2.2 · L3 · Interview

Predict first

A type takes its clock through a defaulted initialiser. What do production and a fixed test clock produce?

protocol Clock { func now() -> Int }
struct SystemClock: Clock { func now() -> Int { 100 } }
struct FixedClock: Clock { let t: Int; func now() -> Int { t } }
struct Greeter {
    let clock: Clock
    init(clock: Clock = SystemClock()) { self.clock = clock }
    func greet() -> String { clock.now() < 12 ? "Morning" : "Afternoon" }
}
print(Greeter().greet(), Greeter(clock: FixedClock(t: 9)).greet())
  • Afternoon Morning
  • Morning Afternoon
  • Morning Morning

Mental model. DI is handing a cook the ingredients instead of sending them to shop. In tests you hand over plastic fruit; in the real kitchen the real produce, and the cook’s recipe does not change.

Dependency injection (DI) means an object receives the things it depends on instead of creating or looking them up itself. The benefits are testability (substitute a fake clock, a stub service, an in-memory repository), flexibility (swap implementations, configure per environment) and explicit dependencies (the initialiser is the list of what the type needs). In Swift the simplest and best form is initialiser injection with a protocol type: init(service: UserService). For production defaults, a default argument keeps call sites short while tests override it: init(clock: Clock = SystemClock()); Greeter() uses the system clock and Greeter(clock: FixedClock(t: 9)) is fully deterministic in a test. Other forms: property or method injection (for objects the system creates, like view controllers from storyboards, or optional collaborators), the SwiftUI environment (inject through the view hierarchy with @Environment, good for shared services and previews), closures as dependencies (var fetch: (Int) async throws -> User in a struct, the “protocol witness” style that is trivial to stub per test), and generic parameters when you want static dispatch.

The anti-pattern is the service locator or singleton reached from anywhere (Network.shared.fetch(), a global Container.resolve() inside business code): the dependency is hidden, tests must mutate global state (and cannot run in parallel), and you only discover a missing registration at runtime. Where a container earns its place is the composition root: one place, at app start (or per feature), that builds the object graph by hand or with a small factory, and passes concrete objects inward. Hand-written factories are enough for most apps; a DI framework (Factory, Swinject, swift-dependencies) helps when the graph is large, scopes differ (singleton, per-flow, per-screen) and you want compile-time or test-time overrides. Keep these rules: depend on protocols the consumer defines and keep them small, inject at the boundary (not three layers down), do not inject what you can pass as a parameter, avoid optional dependencies that must be set later, beware cycles (extract a third type or use a closure), and decide lifetimes consciously (a shared cache is a singleton by design, a per-screen view model is not).

The default clock says 100, the injected one 9, so the same code gives different, deterministic results.

Common traps

  • Calling a global singleton inside business logic. The dependency is hidden and tests must mutate shared state; inject it.
  • Using a DI container as a service locator everywhere. Resolve only at the composition root and pass objects in.

They might follow up with

  • How do default arguments help? Production call sites stay short, and tests pass a fake explicitly.
  • Where do you build the object graph? A composition root, at app start or per feature.

Related questions

  • 15.2.1 What is a coordinator or router, and how does it compare with SwiftUI’s NavigationStack for deciding who owns…
  • 15.1.1 How do MVC, MVVM, MVP, VIPER, RIBs and Clean compare, and which would you choose?
  • 15.3.1 What is a feature module, how do you draw module boundaries, and how does access control enforce them?

← What is a coordinator or router, and how does it compare with…