Dependency Injection in Swift: A Complete Guide for Beginners

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Does Your Code Need Dependency Injection?



Every iOS developer encounters a situation where a class becomes "fat" and tightly coupled to specific implementations. You change one class, and you have to rewrite dozens of others. Sound familiar? This is the classic problem of tight coupling.



Dependency Injection (DI) is a design pattern that solves this problem. Instead of an object creating its own dependencies, they are "injected" from the outside. This makes the code flexible, testable, and compliant with SOLID principles (specifically, the dependency inversion principle).



In this article, we'll break down what DI is in Swift, what ways there are to implement it, and how it simplifies life for an iOS/macOS developer. We'll use simple examples that are understandable even for a beginner.



What is Dependency Injection? A Simple Example



Imagine you are baking a pizza. The version without DI: you grow the wheat yourself, make the flour, milk the cow for cheese. This is tight coupling. The version with DI: you simply order the ingredients (dependencies) from a supplier. You don't know how the flour is made — you only care about the result.



It's the same in programming. Let's say we have a class OrderService that sends orders via email:



// ❌ Without DI — tight couplingclass OrderService {    let mailer = Mailer() // Creates the dependency itself

func process(order: Order) { mailer.send(email: "New order: \\(order.id)") }}


The problem: we cannot replace Mailer with a test version. The OrderService class "knows" too much.



// ✅ With DI — dependency is injectedclass OrderService {    let mailer: MailerProtocol // Depends on an abstraction, not a concrete implementation

init(mailer: MailerProtocol) { // Injection through the initializer self.mailer = mailer }

func process(order: Order) { mailer.send(email: "New order: \\(order.id)") }}


Now we can pass any object that conforms to the MailerProtocol — a real mailer or a "stub" for tests.



Three Main Ways to Inject Dependencies in Swift



There are three popular ways to pass a dependency to a class. Let's look at each.



1. Constructor Injection (via initializer)

The most common and preferred method. The dependency is passed when the object is created. This guarantees that the object is always in a valid state.



protocol NetworkServiceProtocol {    func fetchData() -> String}

class RealNetworkService: NetworkServiceProtocol { func fetchData() -> String { return "Real data from the server" }}

class MockNetworkService: NetworkServiceProtocol { func fetchData() -> String { return "Test data" }}

class DataManager { private let network: NetworkServiceProtocol

// Dependency is passed via init init(network: NetworkServiceProtocol) { self.network = network }

func load() -> String { return network.fetchData() }}

// Usagelet realManager = DataManager(network: RealNetworkService())let mockManager = DataManager(network: MockNetworkService())


Pros: It's clearly visible which dependencies are needed. The object is ready to use immediately. Cons: If there are many dependencies, the initializer becomes cumbersome.



2. Property Injection (via property)

The dependency is set after the object is created. Often used when the dependency is optional or difficult to obtain at the time of initialization.



class UserViewModel {    var apiService: NetworkServiceProtocol? // Optional dependency

func loadUser() { apiServi

Blogs

Book Recommendations