Skip to content

Dependency Injection ​

Dependency Injection gives an object the services it needs instead of letting it construct or locate them globally. The dependency is explicit at the initializer or method boundary.

When it helps in iOS ​

A saved-articles screen loads data from a server in production but needs predictable articles in previews and tests. Injecting a repository keeps the view model independent of networking.

Original example: production and preview repositories ​

swift
import Foundation

struct Article: Identifiable {
    let id: UUID
    let title: String
}

protocol ArticleRepository {
    func savedArticles() async throws -> [Article]
}

struct PreviewArticleRepository: ArticleRepository {
    func savedArticles() async throws -> [Article] {
        [Article(id: UUID(), title: "A sample article")]
    }
}

@MainActor
final class SavedArticlesModel {
    private let repository: any ArticleRepository
    private(set) var articles: [Article] = []

    init(repository: any ArticleRepository) {
        self.repository = repository
    }

    func load() async throws {
        articles = try await repository.savedArticles()
    }
}

let previewModel = SavedArticlesModel(repository: PreviewArticleRepository())

The composition root creates the real repository and passes it to the model. A preview or test supplies PreviewArticleRepository without changing feature code.

Production decisions ​

Use initializer injection for required services and method parameters for one-off inputs. Keep protocols small and tied to the consumer's needs. A huge service locator with resolve() calls hides dependencies again. For SwiftUI, create the model once at an owning view or app root and pass it down.

Map the dependency graph ​

SavedArticlesModel requires an ArticleRepository. Its initializer makes that requirement visible and guarantees the model cannot exist without one. The protocol describes what the consumer needs: loading saved articles. PreviewArticleRepository proves the model can run without a server. In production, the app's composition root creates an API-backed repository and injects it at the same point.

From example to SwiftUI ​

Create the view model once in an owning view or app coordinator, then pass it to the saved-articles screen. A preview supplies the deterministic repository; a unit test can supply one that returns an error or waits on a controllable continuation. The view model maps results into loading, empty, content, and failure states. The repository handles transport and decoding. This makes failures testable without network access or global overrides.

Failure modes and verification ​

A protocol for every tiny helper can add noise. Introduce an abstraction where behavior truly varies, external state is involved, or tests need control. Avoid calling a global ServiceLocator.resolve() from inside the model, because that hides the same dependency again. Check lifecycle: a per-account repository should be recreated when the account changes, while a stateless URL session wrapper may be reused.

All patterns

Released under the MIT License.