An interesting question comes up when we implement the MVVM pattern – who is in charge of navigation? The view controller should really just be for setting the view and delegating input to the view model, and the view model does the business logic. So where does navigation come in? Should the view know about its parent and its context in the flow? Turns out we can easily solve this problem – and in the process solves the problem of dependency injection – just by introducing coordinators.
AppCoordinators and FlowCoordinators are an extremely effective design pattern that allow you to remove navigation responsibility away from the UIViewController and allows you to perform dependency injection easily without the need for a third party library.
Because of how the UITabBarController works in iOS, there are two ways of implementing this pattern – one for no tab bar and one for if you have a tab bar. Where I work, we haven’t ever created an app that doesn’t have a tab bar, so this is the one I’ll focus on. This approach is also a lot less detailed on the web.
Step 1: The AppCoordinator:
This is a class created by the AppDelegate in didFinishLaunchingWithOptions. It holds the tab bar, all the tabs, and the UIWindow of the app. That’s it. The tabs are an array of a custom class called AnyTabCoordinator, which is just a stripped down, non-generic version of a protocol called RootTabCoordinator. So for each tab you want, you create a new class and make it conform to the RootTabCoordinator protocol. Then in your AppCoordinator, you instantiate all these classes in an array and set it to the tabs property:
Step 2: Initialise it in the App Delegate
In didFinishLaunchingWithOptions, we just create a window and start the AppCoordinator:
Step 3: The RootTabCoordinators
For each tab you want, create a class that conforms to RootTabCoordinator, and in the init method for that class, instantiate your first view controller for that tab and use that as the root view controller for the tab, either by wrapping it in a UINavigationController or just directly setting it to the rootController property, which is generic.
Now you can easily inject your instances into view controllers and view models all in the coordinator. If your app has another flow within a tab, you can easily create a new ChildCoordinator conforming class and add it as a child coordinator to your RootTabCoordinator.
And that’s it! Now in your view controllers, when the user presses a button, instead of creating a storyboard segue from the button to the next view controller, you just have a closure that you call and is picked up by your RootTabCoordinator, and that decides what method to call next. If MVVM is properly followed, you view controller files should only take the view model as a dependency, and your view models and other dependencies can take their dependencies in their init methods.
You can see this whole flow, all the required protocols and classes for this to work (under Utils -> MVVM+DI -> Coordinator), including the project structure I thought best fit this pattern, here (checkout the tabBarCoordinator branch):
There’s also another approach that doesn’t use a UITabBarController which is under the feature/coordinator branch.
Generic Dependency Injection in Swift
Here’s a more generic approach to dependency injection, meaning each view controller has a static method that takes its dependencies as a parameter and returns itself.
First, there’s an Injectable protocol. This just defines two things – an assosiatedtype called Dependencies and a variable which it the type of this Dependencies type.
Now for each dependency we have (which are all protocols remember), we create a new protocol which just defines that it has the dependency. For example, we have a MainViewModelConformable protocol. We’d then create a new protocol like:
In our MainViewController, we conform this to Injectable protocol. For the Dependencies assosiatedtype, we set this to HasMainViewModel.
The only thing left is to define a module where all our dependencies are defined. We can do this efficiently with a class and computed properties. You can have as many modules as you like depending on however you want to separate your dependencies. I like to have a new module for each coordinator that I have. Each RootTabCoordinator now has a new generic property of something that inherits from the Module class. So for my MainTabCoordinator, I have a property of let dependencies = MainTabModule(), because MainTabModule inherits from Module. This class looks like this:
Now when we want an instance of our MainViewController, we can just do:
And that will give us a fully injected view controller. Accessing these dependencies in the view controller is just dependencies.secondViewModel, and because of Swift’s protocol composition we can only see the dependencies in the model class that we have said we want and we can easily ask for more dependencies by just changing the Dependencies typealias on the view controller.
Explaining this through words might not be the easiest way of understanding this, so instead I have created a branch on the template app that demonstrates this for you.