Dependency Injection (DI) is a design pattern that enables you to inject dependencies into objects rather than having them instantiate those dependencies directly. This practice improves code modularity, testability, and flexibility, making it easier to build and maintain large applications. In Swift and SwiftUI, DI is commonly used for managing external dependencies, such as network services, data models, or database instances.
For instance, a view that needs access to a data service can have the service passed in as a dependency rather than creating it directly. This approach allows for easy replacement of the service in different contexts, like testing.
Protocol injection
In order to be able to replace the injected object with a different one, we will create a protocol
, which will be the actual type injected, being able to use any object that conforms that protocol.
Let's see an example:
protocol DataService {
func fetchData() -> String
}
class NetworkDataService: DataService {
func fetchData() -> String {
return "Data from network"
}
}
class NetworkDataServiceMock: DataService {
func fetchData() -> String {
return "Mocked data"
}
}
let runTimeService: DataService = NetworkDataService()
runTimeService.fetchData() // "Data from network"
let mockService: DataService = NetworkDataServiceMock()
mockService.fetchData() // "Mocked data"
Let's use this with different types of injections:
Types of Dependency Injection
The most common ways to inject a dependency are:
- Constructor Injection: Injecting dependencies through the initializer.
- Property Injection: Injecting dependencies through properties after object creation.
1. Constructor Injection Example
Constructor injection is a straightforward way to inject dependencies. Hereβs a basic example where we inject a service dependency through the initializer:
class ViewModel {
let dataService: DataService
init(dataService: DataService) {
self.dataService = dataService
}
func getData() -> String {
return dataService.fetchData()
}
}
let viewModel = ViewModel(dataService: NetworkDataService())
let testViewModel = ViewModel(dataService: NetworkDataServiceMock())
In this example, ViewModel
depends on DataService
. By using constructor injection, we can pass any implementation of DataService
to ViewModel
, making it flexible and easy to test.
2. Dependency Injection in SwiftUI
SwiftUI provides the @Environment
property wrapper, a type of dependency injection that allows you to share data across views without manually passing it to each child. This approach is beneficial when using a shared state or view model.
Before using our DataService
as an @Environment
variable, we need to do two small things:
Mark our services as @Observable
:
@Observable
class NetworkDataService: DataService {
...
@Observable
class NetworkDataServiceMock: DataService {
...
And declare our DataService
as an @Environment
variable, providing a default value:
extension EnvironmentValues {
@Entry var dataService: DataService = NetworkDataService()
}
Then we can use it directly in our view:
struct ContentView: View {
@Environment(\.dataService) var dataService
@State var responseLabel: String = ""
var body: some View {
Text(responseLabel)
.onAppear {
responseLabel = dataService.fetchData()
}
}
}
NetworkDataService
will be used as default unless we indicate a different one using the .environment
modifier.
We can do this on our ContentView
preview:
#Preview {
ContentView()
.environment(\.dataService, NetworkDataServiceMock())
}
Be the first to comment