Dependency Injection in iOS

Dependency Injection (DI) in Swift is a design pattern that allows to inject dependencies into a class instead of letting the class create them itself. This improves testability, modularity and flexibility.

There are three main types of Dependency Injection in Swift:

1. Constructor Injection (Initialiser Injection)

2. Property Injection

3. Method Injection

1. Constructor Injection (Initialiser Injection)

Dependencies are passed via the initialiser.

protocol DataService {
    func fetchData() -> String
}

class APIService: DataService {
    func fetchData() -> String {
        return "Data from API"
    }
}

class DataManager {
    private let service: DataService

    init(service: DataService) { // Injected Dependency via Initialiser
        self.service = service
    }

    func getData() -> String {
        return service.fetchData()
    }
}

let apiService = APIService()
let dataManager = DataManager(service: apiService)
print(dataManager.getData()) // Output: "Data from API"

2. Property Injection

The dependency is injected via a property after the object is initialised.

protocol DataService {
    func fetchData() -> String
}


class DataManager {
    var service: DataService?

    func getData() -> String {
        return service?.fetchData() ?? "No data"
    }
}

let dataManager = DataManager()
dataManager.service = APIService()
print(dataManager.getData()) // Output: "Data from API"

3. Method Injection

Dependencies are passed as parameters to methods instead of being stored in properties.

class DataManager {
    func getData(using service: DataService) -> String {
        return service.fetchData()
    }
}

let dataManager = DataManager()
let apiService = APIService()
print(dataManager.getData(using: apiService)) // Output: "Data from API"

Dependency Inversion Principle(DIP)

This principle helps in achieving loose coupling between components, making the code more maintainable, scalable and testable.

Example Without Dependency Inversion (Tightly Coupled Code)

class APIService {
    func fetchData() -> String {
        return "Data from API"
    }
}

class DataManager {
    private let apiService = APIService() // Tight coupling

    func getData() -> String {
        return apiService.fetchData()
    }
}

Here, the DataManager directly depends on APIService creating a tight coupling

If we want to replace APIService with another data source ( MockService for testing) we need to modify DataManager.

Difficult to test because DataManager is tightly coupled to APIService.

To follow DIP, we introduce an abstraction (protocol) so that DataManager depends on an interface, not a concrete class.

// Abstraction (Protocol)
protocol DataService {
    func fetchData() -> String
}

// Concrete Implementation
class APIService: DataService {
    func fetchData() -> String {
        return "Data from API"
    }
}

// Another Implementation (For Testing or Alternate Source)
class MockService: DataService {
    func fetchData() -> String {
        return "Mock Data"
    }
}

// High-Level Module (Now depends on abstraction)
class DataManager {
    private let service: DataService

    init(service: DataService) {
        self.service = service
    }

    func getData() -> String {
        return service.fetchData()
    }
}

// Usage
let apiService = APIService()
let dataManager = DataManager(service: apiService)
print(dataManager.getData()) // Output: "Data from API"

// For Testing
let mockService = MockService()
let testDataManager = DataManager(service: mockService)
print(testDataManager.getData()) // Output: "Mock Data"

Leave a Reply

Your email address will not be published. Required fields are marked *