Intro
When it comes to SwiftUI, we frequently rely on the @StateObject
property wrapper for effectively handling the lifecycle of objects that persist across view updates. However, the process of injecting dependencies into a @StateObject
can feel a bit counterintuitive, particularly if you’re new to the concept. If you’re like me and often find yourself resorting to google just to recall the correct syntax, you’re in the right spot.
This form of dependency injection, where external dependencies are provided to a ViewModel rather than being internally created or managed by the ViewModel itself, is a commonly used practice. It contributes to modularity, testability, and maintainability of a codebase, because it enables us to easily switch between different dependency implementations without having to alter the fundamental functionality of the ViewModel.
Code Example
Let’s say you have a view model that relies on a network service:
1
2
3
4
5
6
7
8
9
10
11
class NetworkService {
// ... network-related logic
}
class ViewModel: ObservableObject {
@Published var data: [String] = []
init(networkService: NetworkService) {
// Initialize and use the network service
}
}
Now, we want to use this view model in a SwiftUI view and inject the NetworkService dependency. Here’s how we can do it:
1
2
3
4
5
6
7
8
9
10
11
struct ContentView: View {
@StateObject private var viewModel: ViewModel
init(networkService: NetworkService) {
_viewModel = StateObject(wrappedValue: ViewModel(networkService: networkService))
}
var body: some View {
// Use the viewModel in your UI
}
}
By using the StateObject property wrapper’s initializer in the init block, we can inject the necessary dependencies into our view model. This ensures that the view model retains its state across view updates while correctly initializing its dependencies.
Another Example
Now, consider a scenario where the dependency we’re injecting into our ViewModel happens to be a published property. The approach remains consistent with what we discussed above.
1
2
3
4
5
struct Task {
let id: UUID
var title: String
var isCompleted: Bool
}
1
2
3
4
5
6
7
8
class TaskViewModel: ObservableObject {
@Published var task: Task
init(task: Task) {
self.task = task
}
}
1
2
3
4
5
6
7
8
9
10
struct TaskView: View {
@StateObject private var viewModel: TaskViewModel
init(task: Task) {
_viewModel = StateObject(wrappedValue: TaskViewModel(task: task))
}
var body: some View {
}
}
Conclusion
So, to wrap things up, injecting a model into a SwiftUI ViewModel is a pretty nifty move that facilitates better modularity, and testing.