Dependency Injection & Testability
iOS Development — Architecture & Data
Chapter 5 · Dependency Injection & Testability
Chapter 3's own APIClient is a real improvement over scattered networking code — but if a
ViewModel creates its own APIClient internally, there's still no real way to test that
ViewModel without hitting an actual real network. This chapter covers the fix: dependency injection.
The Real Problem
refresh() would genuinely hit the real network — slow, flaky, and dependent on
a real server actually being reachable and returning predictable data. There's no real way to substitute
a fake, predictable response without changing TaskListViewModel's own source code.
The Fix: Depend on an Abstraction, Not a Concrete Type
Real dependency injection means a type receives its own dependencies from outside, rather
than creating them itself — and depending on a real protocol, not the concrete
APIClient, is what actually makes substituting a fake possible.
TaskListViewModel() still works exactly as
before, thanks to the real default parameter value. But a test, or a SwiftUI Preview, can now pass in
anything conforming to APIClientProtocol — including a fake that returns predictable
data instantly, with no real network involved.
A Real Fake for Tests and Previews
APIClient() would either hang waiting on a real network call inside Xcode's own canvas, or
show empty state. FakeAPIClient gives a Preview real, immediate, predictable sample data.
SwiftUI's Own Native DI: Custom @Environment Values
For app-wide shared services — reaching many unrelated screens, not just one ViewModel's own initializer
— SwiftUI offers a second, real, framework-native form of dependency injection: a custom
EnvironmentKey.
| Approach | Real Best Fit |
|---|---|
| Initializer Injection | One ViewModel's own specific dependency — explicit, real, and directly testable per-instance |
Custom @Environment | A real, genuinely shared service many unrelated Views need, without threading it through every intermediate initializer |
Hands-On Exercises
Define a real protocol QuoteFetching with one method, func fetchQuote() async throws -> Quote, make Quote's own real fetch function conform via a struct, and refactor QuoteHeaderView (from Fundamentals' capstone) to accept a QuoteFetching value via its own initializer instead of calling fetchQuote() directly.
Write a real FakeQuoteFetcher: QuoteFetching returning a fixed, predictable Quote instantly, with no real network call — then use it in a #Preview for QuoteHeaderView.
Explain, in your own words, why depending on APIClientProtocol rather than the concrete APIClient struct is specifically what makes substituting FakeAPIClient possible, and why a default parameter value on the initializer means existing, real app code needs no changes at all.
Chapter 5 Quick Reference
- A type creating its own dependencies internally is a real, genuine testing dead end
- Depending on a real
protocol, not a concrete type, is what makes substituting a fake possible - Initializer injection with a default parameter value keeps ordinary app code unchanged while enabling real tests/Previews to substitute a fake
- A real fake conforming to the same protocol returns predictable data instantly — no real network involved
- A custom
EnvironmentKey/@Environmentvalue is SwiftUI's own native DI mechanism for services shared across many unrelated Views