And saying good-bye to the massive ViewControllers.
We’ve all been there, in the entangled mess of controllers that perfectly shows how MVC got its nickname “Massive ViewController”.
It’s a far cry from the modular, easily maintainable and scalable app we all aim for and it comes with its own set of problems. One part of those problems can be amended by dropping the MVC architecture in favor of MVVM, but with others you need extra help.
One of them is an over-complicated navigation flow, which not only slows down the development, but it also makes the code harder to debug.
Luckily, there’s an answer to that: Coordinators.
From the desk of Ante Baus, our senior iOS engineer.
How do ViewControllers grow into monsters?
This seems simple enough, but will prove very wrong for a number of reasons.
The Navigation Controller is a parent of MasterVC, and yet its child commands it. In the best case scenario, the child controller shouldn’t even know who its parent is, let alone command it.
We expect it to act the same regardless of whether it’s presented, a part of the navigation stack, added as a child to a ContainerVC or displayed in a TabController.
It’s perfectly fine for a child ViewController to delegate some work, even if it delegates them to the parent. However, when the child VC has a reference to the parent ViewController and directly calls its methods, the whole thing becomes problematic.
The fact that a UIViewController has a reference to UINavigationController in this example violates the parent-child hierarchy.
Here we can see that the MasterVC “knows” exactly what’s behind it in the navigation stack, prepares the data and configures the next view controller in line. Not only does this cause excessive coupling, it also gives the ViewController too much responsibilities. In the end, the ViewController in question will become an absolute monster that’s hard to both maintain and debug.
Another problem is that the data displayed in ViewControllers isn’t centralized. Imagine a situation where you have the following stack:
And what if you needed to add another VC between the VC2 and VC3 sometime in the future? The line grows longer and more complicated to debug, and for no good reason.
In this navigation, every ViewController knows what’s behind it, but there isn’t a single object that has the overview of the entire stack and could easily forward information or add and remove ViewControllers from the stack.
Slaying the monster with coordinators
It’s that one formerly mentioned object that has a bird’s-eye view over the whole situation and can take on some of the responsibilities away from the ViewControllers.
Now, let’s talk structure. The rule of the thumb is that one coordinator is used for one logical unit. That way, you break down the app logic into modular, easily manageable chunks.
For example, if you have a flow for sending a photo to the server, one coordinator will be in charge of the ViewControllers for taking the photo, photo manipulation and upload screen, respectively.
Coordinators can also further be organized in a parent-child hierarchy. The tree begins with the root coordinator which then displays one of its children coordinators, depending on the state that the app is in at the given moment.
The most common example is when the app needs to display either the authentification flow, for users who are not logged in, or the home screen, for the users who are.
This is how we usually handle that here at DECODE:
AuthenticationCoordinator is then in charge of all the screens related to authentication. After the user has been successfully authenticated, through either login or signup, onEnding block is executed.
This can also be handled with a delegate, but we prefer to do it this way to avoid extra code and files, keeping things as clean as possible.
And this is how we start and initialize the root coordinator:
Rejoice, the VC monster is tamed!
There are several benefits to this approach:
1. There is no coupling of ViewControllers. One ViewController needs not know anything about the one which follows after a certain action. The only thing it needs to do is alert the coordinator that a certain action happened via delegate or closure (for example, that the user has been authenticated).
2. Every ViewController can be reused anywhere within the app, and the coordinator decides how it will be displayed (as a push navigation controller, in a tab controller or any other way).
3. More controllers can be grouped in one logical unit, which enables us to reuse the whole component (like in the example of taking a photograph and sending it to the server).
4. Since the coordinator is a regular object, the developer has all the control over it, and in turn, complete control over the flow.
In the end, you end up with a more modular, easily manageable code that will save you time in the long run. And that’s the goal, right?