Container DI in SwiftUI

Maksim Po
2 min readDec 23, 2023

--

Dependency Injection takes important place in development Applications. Today many frameworks are known for helping with DI in Swift.

This article is how to implement DI in SwiftUI with native instruments. The basis for my thoughts was working with SwiftData on iOS 17.

First you need to decide where to start. Usually the DI starts at the very beginning, while the user sees only the loading screen. The application may have many dependencies and the download may take a long time. For the purposes of this article, we will skip this point.

To organize the DI simply and accurately, we will use ViewModifier.
in it we will register our services and at the time of initialization it self we will initialize the Services.

struct ServicesDIContainerModifier: ViewModifier {
let networkService: NetworkServiceProtocol
let locationService: LocationServiceProtocol

init() {
networkService = NetworkServiceImplementation()
locationManager = LocationServiceImplementation()
}

func body(content: Content) -> some View {
content
.environmentObject(networkService)
.environmentObject(locationService)
}
}

It seems that this could be completed. But we’ll make it a little more beautiful. As said, when you work with a custom modifier, it’s a good idea to also create a View extension that makes modifier easier to use. So we need to make a modifier in the View extension.

extension View {
func servicesDIContainer() -> some View {
modifier(ServicesDIContainerModifier())
}
}

And the final step will be to apply the modifier to the base View. Applying the modifier to the very “first” view is necessary so that further services are available in the entire hierarchy of views / subviews / subviews, etc.

@main
struct MyApp: App {

var body: some Scene {
WindowGroup {
ContentView()
.servicesDIContainer()
}
}
}

In my opinion, it turned out to be a compact and neat DI container.
Then all that remains is to remember how to access services in the view.
And with this we will be helped by the @EnvironmentObject property wrapper, which is responsible for the connection with environment objects.

@EnvironmentObject var networkService: NetworkServiceProtocol

And in general the view code will look like this. Where we can work with services as we need, through the example of DI.

struct MainScreen: View {
@EnvironmentObject var networkService: NetworkServiceProtocol

var body: some View {
MyView()
.task {
networkService.locationJobs()
}
}
}

This concept allows you to create a simple DI container for your services in a native way, avoiding the creation of numerous entities of services in the each view. We also avoid using the powerful singleton pattern, which has its drawbacks, despite its simplicity.

--

--