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"