Dependency Injection in Swift: Principles and Examples for iOS/macOS

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 to Dependency Injection in Swift



Dependency Injection (DI) is one of the key design principles that helps create flexible, testable, and maintainable applications in Swift. In the context of iOS and macOS development, DI allows separating object creation from their usage, which significantly simplifies testing and code modification.



The main idea is that a class should not create its dependencies on its own. Instead, dependencies are "injected" from the outside. This is an implementation of the Dependency Inversion Principle from SOLID.



Why is Dependency Injection important for iOS/macOS development?



In native Swift applications, the problem of tight coupling often occurs. Let's look at a typical example:



class UserService {    let networkManager = NetworkManager()    let database = Database()        func fetchUser(id: Int) -> User {        // logic    }}


Here UserService is tightly coupled to specific implementations of NetworkManager and Database. This creates several problems:



  • Testing complexity — it's impossible to replace real services with mocks or stubs
  • Low reusability — the class cannot be used with other implementations
  • SRP violation — the class handles not only business logic but also dependency creation


Dependency Injection solves these problems by allowing dependencies to be injected through a constructor, properties, or methods.



Ways to implement Dependency Injection in Swift



1. Constructor Injection



The most common and recommended approach. Dependencies are passed through the initializer:



protocol NetworkService {    func request(endpoint: String) -> Data}

protocol DatabaseService { func save(user: User)}

class UserService { private let networkService: NetworkService private let databaseService: DatabaseService init(networkService: NetworkService, databaseService: DatabaseService) { self.networkService = networkService self.databaseService = databaseService } func fetchUser(id: Int) -> User? { let data = networkService.request(endpoint: "users/\\(id)") // data parsing return User(id: id, name: "John") }}


Advantages: dependencies are clearly visible, mandatory for the class to work, easy to test.



2. Property Injection



Dependencies are set after object creation:



class AuthManager {    var networkService: NetworkService?        func login(username: String, password: String) -> Bool {        guard let service = networkService else { return false }        // authentication logic        return true    }}

// Usagelet authManager = AuthManager()authManager.networkService = RealNetworkService()


Disadvantage: dependencies may not be set, leading to optional types and potential crashes.



3. Method Injection



The dependency is passed directly to the method:



class DataProcessor {    func process(data: Data, using service: NetworkService) -> ProcessedData {        let result = service.request(endpoint: "process")        // processing        return ProcessedData()    }}


Suitable for cases when a dependency is needed only for one method.



Practical example: task management application



Let's create a simple iOS application using DI:



// Dependency protocolsprotocol TaskRepository {    func fetchTasks() ->                

Blogs

Book Recommendations