Skip to content
Prepare to Grow

iOS Architecture

Dependency Injection

Make collaborators and lifetimes explicit through initializer injection, composition roots, and focused test replacements.

Advanced6 min read2 guided activities

Before you begin

What you will understand

  • Explain dependency injection without relying on a framework.
  • Choose initializer, method, property, or environment injection deliberately.
  • Build a visible composition root.
  • Manage dependency lifetimes and concurrency safely.
01

Learning stage

Concept Explanation

Dependency injection means an object receives the collaborators it needs instead of constructing or locating them secretly. Initializer injection is the default because it makes valid construction explicit and allows immutable dependencies. Method injection fits a dependency needed for one operation; property or environment injection is appropriate only when the framework owns construction and missing values are handled safely.

The composition root is the narrow place where concrete implementations are chosen and connected. In an iOS app it may begin in the App type, scene setup, application delegate, or root coordinator. It can create long-lived actors and repositories once, then construct feature-scoped models and controllers as routes begin.

Injection does not require a container library or a protocol for every type. Concrete stable values can be injected directly, and a closure can model one small capability. The design must still define lifetime, actor isolation, and ownership: a shared mutable service is not safe merely because it arrived through an initializer.

  • Initializer injection: required dependency supplied at construction.
  • Method injection: capability supplied for one operation.
  • Composition root: selects and connects concrete implementations.
  • Lifetime: how long a dependency instance is retained and shared.
  • Test double: controlled replacement such as a fake, stub, spy, or clock.
  • Service locator: global lookup that hides requirements at call sites.
02

Learning stage

Syntax & Examples

One dependency graph makes live choices visible while feature factories create correctly scoped presentation models.

AppDependencies.swiftswift
struct AppDependencies: Sendable {
  let courses: any CourseRepository
  let progress: any ProgressSaving
  let analytics: any AnalyticsRecording
  let now: @Sendable () -> Date

  static func live() -> AppDependencies {
    let client = APIClient(configuration: .production)
    let cache = SQLiteCourseCache()

    return AppDependencies(
      courses: LiveCourseRepository(
        remote: APICourseDataSource(client: client),
        cache: cache
      ),
      progress: APIProgressStore(client: client),
      analytics: AnalyticsService(),
      now: Date.init
    )
  }

  @MainActor
  func makeCourseListModel() -> CourseListViewModel {
    CourseListViewModel(loader: courses)
  }
}

@main
struct LearningApp: App {
  private let dependencies = AppDependencies.live()

  var body: some Scene {
    WindowGroup {
      CourseListView(model: dependencies.makeCourseListModel())
    }
  }
}
The root selects live implementations; features receive only the collaborators required for their construction.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Use deterministic clocks and UUID generators in tests.
  • Swap live repositories for in-memory preview data.
  • Share one authenticated API client while creating feature-scoped models.
  • Keep environment-specific endpoint and analytics choices at the app boundary.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Accessing dependencies through a global static container from anywhere.
  • Giving every feature the complete application dependency graph.
  • Creating a protocol for every concrete value without a boundary need.
  • Using force-unwrapped property injection that can produce invalid objects.
  • Ignoring whether a shared dependency is Sendable or actor-isolated.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Build a composition root

Create live, preview, and test dependency graphs for a course feature. Document which instances are application-, flow-, and screen-scoped.

Hint: Feature factories can narrow the large root graph into small initializer requirements.

Activity 2

Remove a service locator

Refactor code that reads APIClient.shared and Date() directly so requirements become visible and tests become deterministic.

Hint: A closure is often enough for a clock or ID generator.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

Why is initializer injection usually preferred?

  1. A.It makes required dependencies and valid construction explicit
  2. B.It removes every initializer
  3. C.It creates global state
  4. D.It prevents all reference types
Reveal answer

It makes required dependencies and valid construction explicit

The object cannot exist in a partially configured state and its requirements are visible to callers.

QUESTION 2

What is the composition root responsible for?

Reveal answer

Choosing concrete implementations, defining lifetimes, and assembling the dependency graph.

Business and presentation code consume dependencies without deciding how live infrastructure is constructed.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

How do you implement dependency injection in an iOS application?

Short answer: I use normal Swift initializers and feature factories, assembling live implementations in a small composition root.

Explore the detailed answer

Required collaborators use initializer injection, one-off values may use method injection, and SwiftUI environment values are reserved for genuinely shared framework-owned context. The root controls application, flow, and screen lifetimes. Tests supply focused fakes or closures, and concurrency contracts remain explicit.

Possible follow-ups

  • • When would you use a DI container?
  • • How do you avoid passing a huge dependencies object everywhere?
08

Learning stage

Key Takeaways

Dependency injection is explicit construction and ownership. It makes requirements visible, replacements straightforward, and lifetimes deliberate without requiring a special framework.

  • Prefer initializer injection for required collaborators.
  • Assemble live choices at a composition root.
  • Narrow dependencies at feature boundaries.
  • Use closures for small capabilities.
  • Define lifetime and concurrency, not only type conformance.