iOS Architecture: MVVM-C, Coordinators (3/6)
What is a Coordinator?
This is the last part of this architecture’s name but I think it’s the most important part. If you can only implement one part of this architecture as a whole I would recommend the you implement this pattern, as I think it has incredible potential to improve your overall application structure.
A coordinator is an object (Class type in Swift) which has the sole responsibility, as it’s name implies, to coordinate the App’s navigation. Basically which screen should be shown, what screen should be shown next, etc.
This basically means that the coordinator has to:
- Instantiate ViewController’s & ViewModel’s
- Instantiate & Inject dependencies into the ViewController’s and ViewModel’s
- Present or push ViewController’s onto the screen
(Don’t worry about ViewModels right now, we will look at them in the next article)
Why use a coordinator?
Coordinators are a great tool because they free our ViewController’s from a responsibility that they should not have. This helps us adhere to the single responsibility principle, which makes our ViewController’s much leaner and easier to re-use.
A ViewController should not know what ViewController came before it, which one should come next, or what dependencies it should pass on. It should just be a “dumb” wrapper around the View/Subviews on the screen, and handle only UIKit related stuff. Any action regarding navigation on the ViewController should be sent up to the Coordinator for proper handling.
Coordinator base class
Let’s see some code. We start with a an abstract base class that all our Coordinator’s will inherit from.
This base class is not absolutely neccesary, you could use a protocol if you prefer. But I find that this way of organizing coordinator’s makes sense and has worked well for me.
If you want to create your own coordinator you first subclass this base class, and then override the start and finish methods. Then you have built in methods to handle adding and removing child coordinator’s. Very similar to how a UIViewController works.
Your app should consist of multiple coordinators, one for each scene. But it should always have one “main” AppCoordinator, which will be owned by the App Delegate.
The following code is an example of a basic AppCoordinator. All other coordinators in the application will be children of this coordinator.
Let’s go over it, there are 3 important part’s here:
- Properties: These are the properties this object owns. In this case since it’s the AppCoordinator it must own the window. It also owns the root and other ViewController’s, and any dependencies it will need, like the ApiClient in this case.
- Init method: Here you must take in any dependencies you will need in this coordinator. In our case since it’s the first coordinator the only dependency we need is the app’s window.
- Coordinator: Here you must override the Start() and Finish() methods. The start method is the most important one, which is where you present your UI onto the screen. In this case what we do is set our root view controller onto the window.
This is how you would “start” your application from the AppDelegate using the AppCoordinator.
You will hold onto the AppCoordinator as a property in your AppDelegate, the same way that the window is held on to. Then in the application: didFinishLaunchingOptions: method of your AppDelegate you instantiate the App Coordinator with the UIWindow, then call start() on it.
Your app will obviously need at least one, if not many more, child coordinator’s to handle all the scenes in your app.
Let’s go over an example of this kind of coordinator so you can get an idea. It is going to be similar to the AppCoordinator but it’s going to have some more responsibilities, depending on how complicated your scene’s are.
This part is very similar to our AppCoordinator, but let’s go over it anyways.
- Properties: We have a reference to the storyboard related to this coordinator/scene, and all other view controller’s and dependencies.
- Init, Root View Controller: This coordinator is initialized with a root ViewController, not a window like in the AppCoordinator.
- Init, Api Client: Notice that we also get the ApiClient in the initializer. As I said regarding the AppCoordinator we should get all the dependencies we are going to need in the initializer. Since the ApiClient already has important session headers we want to use the same one all around our app. This is one of the biggest benefits of using coordinators, you don’t really have to use Singletons anymore. Yes, we only have on instance of this ApiClient, and we pass it around using dependency injection, BUT we are not using a static global variable to access it from everywhere which is usually considered an anti-pattern.
- Start: Basically the type of root “container” we are initialized with dictates what we are going to do in the Start() method. In this case, since it’s a TabBarController we will add ourselves to it. If it was a NavigationController we would push ourselves onto it, and if it’s a plain ViewController we would present ourselves on it.
- Finish: Here we have to clean up any ViewControllers we have on the navigation stack, or anywhere else. Tell our service layer to cancel any pending requests. And finally, call our delegate, if we have any, and let it know that we are finished.
Navigation is a big part of Coordinators. I like to add an extension to the coordinator to handle all navigation, having a method for each navigation that can happen.
We will get into where these methods are called from, but for now let’s just focus on them in isolation. I like to name my navigation methods starting with the goTo prefix. You will usually have a bunch of these.
Notice how these two examples are different:
- The first one, goToLocationSearch instantiates a ViewController and presents it on the ViewController passed in as a parameter. Remember, this ViewController is still part of the same scene.
- The second one, goToSearch is different because here we are leaving this scene. This part of the application that we’re navigating to is another scene, so it’s going to be handled by another coordinator. So here we only instantiate the other coordinator, send in any dependencies, like the root ViewController. We call start on it, and most importantly we add it as a child coordinator. If we don’t do this nobody is going to be holding onto the coordinator and it will be released.
Another important part of the coordinator are the delegate callbacks we are going to be getting from the ViewModel’s (or ViewControllers) and other child coordinators.
First, we may have callbacks from our child coordinator’s. If a child coordinator has to inform us of something it’s going to have a delegate protocol we must implement.
In this case we are implementing the didFinishFrom: delegate method of a child coordinator. Here if our child coordinator is finished, it’s finish method should be called, and it’s finish method should call it’s delegate to let it know it’s finished. Afterwards, we remove the child coordinator from our stack. That assures that it’s removed from memory.
Finally, the other kind of callbacks we might have are from our ViewModel’s (or ViewControllers if you are not using ViewModel’s).
These callbacks let us know if the user want’s to navigate to another part of the app, and from this is where we would call our goTo navigation methods. Notice how the ViewController this request is coming to is a paremeter in these delegate methods. This makes it much easier to be able to present view controllers on top of them without having to keep a reference or go looking for the view controller in our hierarchy.
That’s basically it for Coordinator’s. A big part of how our coordinator’s will handle everything has to do with the ViewModel’s so that’s the topic we will discuss next.