This is a post in a series on Advanced Coordinators. If you haven’t read the original post or the longer follow-up, make sure to check those out first. The series will cover a few advanced coordinator techniques, gotchas, FAQs, and other trivia.

When working with coordinators, all flow events should travel through the coordinator. Any time a view controller intends to change flow state, it informs the coordinator, and the coordinator can handle side effects and make decisions about how to proceed.

There is one glaring exception to this rule: when a navigation controller navigates “back”. That back button is not a traditional button, so you can’t add handlers to it to send messages up to the coordinator. Further, its associated behavior is performed directly by the navigation controller itself. If you need to do any work in a coordinator when a view controller is dismissed, you need some way to hook into that behavior.

While there are other less common examples, the primary use case is when you have a sub-flow that takes place entirely within the context of another navigation controller. Coordinators typically own one navigation controller exclusively, but sometimes, a subset of the flow with in a navigation controller stack needs to be broken out into its own coordinator, usually for reuse purposes. That separate coordinator shares the navigation controller with its parent coordinator. If the user enters the child coordinator (entering the sub-flow) and then taps the back button, that child coordinator needs to be cleaned up. If it’s not cleaned up, that coordinator’s memory will effectively be leaked. Further, if they enter that flow a second time, we might have two of the same coordinator, potentially reacting to similar events and executing code twice.

So, we need a way to know that the navigation’s back button has been tapped. The UINavigationControllerDelegate is the easiest way to get access to this event. (You could subclass or swizzle, but let’s not.)

There are a few ways to use this delegate to solve this problem, and I’d like to highlight two of them. The first is Bryan Irace’s approach to tackling this problem. He makes a special view controller called NavigationController that allows you to push coordinators in addition to pushing view controllers.

I’ll elide some of the details and give an overview of the approach, but if you want a full details, I recommend reading his whole post. The main thing to note in his code is:

final class NavigationController: UIViewController {

	// ...

	private let navigationController: UINavigationController = //..
	
	private var viewControllersToChildCoordinators: [UIViewController: Coordinator] = [:]
  
	// ...

}

This shows the way that this class works. When you add a new coordinator to this class, it creates an entry in this dictionary. The entry maps the root view controller of a coordinator to the coordinator itself. Once you have that, you can conform to the UINavigationControllerDelegate.

extension NavigationController: UINavigationControllerDelegate {    
	func navigationController(navigationController: UINavigationController,
		didShowViewController viewController: UIViewController, animated: Bool) {
		// ...
	}
}

At that point, if the popped view controller is found in the coordinator dictionary, it will remove it, allowing it to correctly deallocate.

There’s a lot to like about this approach. Coordinator deallocation is handled automatically for you, when you this class instead of a UINavigationController. However, it comes with a few downsides, as well. My primary concern is that the NavigationController class, which is a view controller, knows about and has to deal with coordinators. This is tantamount to a view having a reference to a view controller.

I think there are some goopy bits on the inside of UIKit where views know about their view controllers. I haven’t seen the source code, but the stack trace for -viewDidLayoutSubviews suggests that there’s some voodoo going on here. Sometimes, components in a library may be coupled together more tightly, in order to make the end user’s code cleaner. This is the tradeoff that Bryan is making here.

If you don’t want to make that tradeoff, you can bring the navigation controller delegate methods to the parent coordinator, where they can live with all the other flow events. This is my preference. By making the coordinator into the delegate of the navigation controller, you can maintain the structure of the coordinator: namely that it is the parent of the navigation controller. When you get the delegate messages that a view controller was popped off, you can manually clean up any coordinators that need to be dealt with.

extension Coordinator: UINavigationControllerDelegate {    
	func navigationController(navigationController: UINavigationController,
		didShowViewController viewController: UIViewController, animated: Bool) {
		
		// ensure the view controller is popping
		guard
		  let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from),
		  navigationController.viewControllers.contains(fromViewController)) else {
			return
    	}
		
		// and it's the right type
		if coordinator is FirstViewControllerInCoordinator) {
			//deallocate the relevant coordinator
		}
	}
}

This approach is slightly more manual, with the up- and downsides that come with that: more control and more boilerplate. If you don’t like the direct type check, you can replace it with a protocol.

You’ll also need to re-enable the interactivePopGestureRecognizer by conforming to UIGestureRecognizerDelegate and returning true for the shouldRecognizeSimultaneouslyWithGestureRecognizer delegate method.

Both approaches are good ways of handling decommisioned coordinators and ensuring that they correctly deallocate, and these techniques are crucial for breaking out your subflows out into their own coordinators so they can be reused.