Swift/UnwrapDiscover about Swift, iOS and architecture patterns

Dependency Injection Container

January 03, 2023

In a previous article Dependency Injection principles I explained what is dependency injection πŸ’‰. Let's now have a look on how to manage and use dependencies through a Dependency Container.

Is a Container mandatory?

Let's be clear: you don't need a Depdency Injection Container to benefit from Dependency Injection. However DI Container can be really helpful when dealing with multiple dependencies, especially to manage their lifecycle.

In essence a DI Container is a type knowing how to build an object and all its dependencies. You can write your own or use a library. We'll do both but let's start by doing our own.

If you did not read Dependency Injection principles or are not very familiar with Dependency Injection concepts then read it first! Using DI Container upon πŸ’© will just bring you bigger πŸ’©πŸ’©.

Creating a DI Container

Let's reuse our MovieViewModel and MovieRepository from Dependency Injection principles and create a Dependency Container for it.

class DependencyContainer {
    func resolveMovieViewModel() -> MovieViewModel {
        MovieViewModel(repository: resolveMovieRepository())
    }

    private func resolveRepository() -> Repository {
        Repository(urlSession: .shared, jsonDecoder: JSONDecoder())
    }
}

As you can see the code is actually fairly identical to what we had before. The difference being the resolution now happens in a dedicated class rather than relying on parameters default value.

One advantage of this approach is that you cannot forget to resolve a dependency: in order to instantiate a MovieViewModel you know need to explicitly give a MoveRepository.

Lifecycle

One neat thing about Dependency Container is how you can configure objects lifecycle.

Here for instance we can quickly swipe our Repository from a created-at-every-call to a shared one (a singleton per se).

class DependencyContainer {
    private lazy var repository = Repository(urlSession: .shared, jsonDecoder: JSONDecoder())
    
    func resolveMovieViewModel() -> MovieViewModel {
        MovieViewModel(repository: resolveRepository())
    }

    /// now we'll always get the same repository instance
    private func resolveRepository() -> Repository {
        repository
    }
}

Swinject

Swinject πŸ’‰ is one of the most popular Dependency injection framework for Swift.

Our previous container could be rewritten as follow:

import Swinject

class AppAssembly: Assembly {
    func assemble(container: Container) {
        container.register(MovieViewModel.self) { resolver in
            MovieViewModel(repository: resolver.resolve(Repository.self)!)
        }

        container.register(Repository.self) {
            Repository(urlSession: .shared, jsonDecoder: JSONDecoder())
        }
        .inObjectScope(.container) // singleton
    }
}

As you can see the code is fairly the same.

Using the container

The most important thing to remember is that Dependency Container manages dependencies lifecycle ♾️. Therefore it's very important to have only one container instance.

One approach to ensure this uniqueness is by instantiating it in AppDelegate.

import Swinject

class AppDelegate {
    private let depencenyResolver = Assembler([AppAssembly()]).resolver
}

How to use it then depends on whether or not you have a router. Let's start with the least probable case 😝: you have a router inside your app.

Routed app

Router pattern πŸ›£ is great because it's a common path where you main app logic always goes through.

Magellan is one such example: it has a Router closure called every time we display a new screen. For people using SwiftUI, NavigationStack and NavigationPath can be used to build such a component.

In this approach our router is in charge of resolving dependencies:

import Magellan

class AppDelegate {
    private let dependencyResolver = Assembler([AppAssembly()]).resolver
    private var navigation: Navigation!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
        setupNavigation()
    }

    func setupNavigation() {
        navigation = Navigation(root: window.rootViewController!) { [dependencyResolver] route, _ in
            switch route {
            case .movies:
                let viewController = StoryboardScene.Main.movies.instantiate()
                viewController.viewModel = MoviesViewModel(
                    repository: dependencyResolver.resolve(MovieRepository.self)!
                )

                return Route(viewController).present(PageSheetPresentation())
        }
    }
}

If you don't have a Router don't worry though! You can still use a dependency container.

The less good but more usual case

With no router you need to access your Dependency Container from every root controllers (or views).

In SwiftUI you can rely on @EnvironmentObject to access and use you container. However be sure to not over use it and access it only from root views (basically your smart views if you've been following Smart/Dumb approach).

import Swinject

struct MyScreen: View {
    @EnvironmentObject dependencyResolver: Resolver

    var body: some View {
        NavigationLink() {
            ....
        }
    }

    @ViewBuilder
    private var moviesScreen: some View {
        let viewModel = MoviesViewModel(repository: dependencyResolver.resolve(MovieRepository.self)!
        
        MovieScreen(viewModel: viewModel)) {
            ...
        }
    }
}

On UIKit you'll have to set the instance on each root view controller.

import Swinject

class RootController: UIViewController {
    var dependencyResolver: Resolver!

    func showMovies() {
        let viewController = StoryboardScene.Main.movies.instantiate()
        
        viewController.dependencyResolver = dependencyResolver
        viewController.viewModel = MoviesViewModel(
            repository: dependencyResolver.resolve(MovieRepository.self)!
        )

        pushViewController(viewController, animated: true)
    }
}

This way you'll be able to access your container and resolve your dependencies.

This is how you can either build your own DI Container or use a third-party library. Which approach to use is mostly a matter of taste although I would advise for libraries as it can greatly simplify object lifecycle.

However libraries might come with a very big tradeoff you probably didn't notice at first glance: compile-time safety πŸ‘·! We'll dive πŸŠβ€β™‚οΈ on this in a future article.