Taming the app flow with coordinators

March 3, 2017

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.

mobile app developers

How do ViewControllers grow into monsters?

Let’s take a look at the snippet of code which uses the standard navigation, like the Apple’s template for a Master — Detail app does:ž

extension MasterVC: UITableViewController {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let detailData = getDetailData(indexPath: indexPath)
    let  detailVC = DetailVC()
    detailVC.detailData = detailData
    self.navigationController.pushViewController(detailVC, animated: true)
  }
}

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, but it also gives the ViewController too many 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:

vc1 vc2 and vc3

Among other things, you need to display a subset of data from the VC1. How would you do it? By forwarding that data to VC2, and then to VC3 from there? In that case, the VC2 receives and forwards data it doesn’t even need.

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 an overview of the entire stack and could easily forward information or add and remove ViewControllers from the stack.

Slaying the monster with coordinators

All this can be easily fixed with coordinators. In short, they are objects that control one or more ViewControllers, and thus the flow of the app.

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.

cordinatros

Since the coordinator is just a plain NSObject, the developer has full control over it. The coordinator can be initialized whenever and started when it is suited and it doesn’t depend on the lifecycle of one super class or the other as the UIViewController does.

Now, let’s talk about 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:

class RootCoordinator {
  func start() {
    if userLoggedIn() {
      showHomeCoordinator()
    } else {
      showAuthenticationCoordinator()
    }
  }
  func showHomeCoordinator() {
    let navigationController = UINavigationController()
    let homeCoordinator = HomeCoordinator(navigationController: navigationController)  
    unowned(unsafe) let coordinator_ = homeCoordinator
    homeCoordinator.onEnding = { [weak self] in
      self?.removeChildCoordinator(childCoordinator: coordinator_)
    }
    addChildCoordinator(homeCoordinator)
    homeCoordinator.start()
  }
  func showAuthenticationCoordinator() {
    let navigationController = UINavigationController()
    let authenticationCoordinator = AuthenticationCoordinator(navigationController: navigationController)
    unowned(unsafe) let coordinator_ = authenticationCoordinator
    authenticationCoordinator.onEnding = { [weak self] in
      self?.removeChildCoordinator(childCoordinator: coordinator_)
    }
  
    addChildCoordinator(authenticationCoordinator)
    authenticationCoordinator.start()
  }
}

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.

Root Coordinator

I’ve already mentioned that the coordinator is a plain NSObject and can be used as we please. However, there is a certain set of methods that keep repeating, so we wrote a general coordinator that all the other coordinators in the app inherit:

class Coordinator {
  // RootViewController for the Coordinator. This ViewController is injected into the Coordinator during the initialization and the Coordinator knows how to use it to display the ViewController it is in charge of.
  let rootViewController: UIViewController
  // List of Child Coordinators. Here is where all the references are stored for all Child Coordinators, so they are not deallocated before they need to be. The Parent Coordinator is in charge of deallocating all the Child Coordinators when the time is right.
  private(set) var childCoordinators: [Coordinator]
    
  // Block which the coordinator must call when it’s done its job. The Parent Coordinator usually deallocates the Child coordinator in this block. 
  var onEnding: (() -> Void)?
  init(rootViewController: UIViewController) {
    self.rootViewController = rootViewController
    self.childCoordinators = []
    }
  
  // Function in charge of adding Child Coordinators 
  func addChildCoordinator(childCoordinator: Coordinator) {
    childCoordinators.append(childCoordinator)
  }
  
  //Function in charge of removing a Child Coordinator
  func removeChildCoordinator(childCoordinator: Coordinator) {
        
    if let index = childCoordinators.index(of: childCoordinator) {
    childCoordinators.remove(at: index)
    } else {
    fatalError("Trying to remove not existing child coordinator")
    }
  }
  // Override this function to start the coordinator
  func start () {
  }
}

And this is how we start and initialize the root coordinator:

class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
  
    private var rootCoordinator: RootCoordinator!
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
    ...
    let rootVC = UIViewController()
    window = UIWindow(frame: UIScreen.mainScreen().bounds)
    window!.rootViewController = rootVC
    window!.makeKeyAndVisible()
    let window = UIWindow(frame: UIScreen.main.bounds)
        
    rootCoordinator = RootCoordinator(rootViewController: rootVC)
    rootCoordinator.start()
        
    return true
  }
    
  ...
}

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?

Categories
Written by

Ante Baus

CDO

Ante is a true expert. Another graduate from the Faculty of Electrical Engineering and Computing, he’s been a DECODEr from the very beginning. Ante is an experienced software engineer with an admirably wide knowledge of tech. But his superpower lies in iOS development, having gained valuable experience on projects in the fintech and telco industries. Ante is a man of many hobbies, but his top three are fishing, hunting, and again, fishing. He is also the state champ in curling, and represents Croatia on the national team. Impressive, right?

Written by

Vladimir Kolbas

IOS Team Lead

When something unusual happens, Vlado is the man with an explanation. An experienced iOS Team Lead with a PhD in Astrophysics, he has a staggering knowledge of IT. Vlado has a lot of responsibilities, but still has time to help everybody on the team, no matter how big or small the need. His passions include coffee brewing, lengthy sci-fi novels and all things Apple. On nice days, you might find Vlado on a trail run. On rainier days, you’ll probably find him making unique furniture in the garage.

Related articles