Inverse Reference Coordinator Pattern on iOS
There are plenty of resources that explain what is the coordinator pattern on iOS (see here for the creator introduction or search it on the web) and how to implement it the right way, which seems to be a lot. The existence of multiples right ways of implementing the coordinator pattern may be because there is no straight way of handling the pop of a navigation controller.
This post does not bring the ultimate implementation of the coordinator pattern but offers a new refreshing perspective to the implementation when handling popping.
The Coordinator Pattern: fast course
Navigation logic in iOS is not a an easy problem. In modern applications, it is desirable that the user can access the same screen from multiples entry points. If using segues or direct instantiation in the controllers, every controller know too much information in the application flow; they know its position. This leads to complex definition of navigation logic inside each controller and how it should behave when coming from one site or another.
The coordinator pattern comes to relieve this. The coordinator is the object that handles the navigation between controllers. Now, the controllers must notify to the coordinator every event of end and it is the coordinator who route to the next controller.
This pattern provides better testability for controllers (it force to expose controller dependencies), extracts navigation logic from the controller to another object (better encapsulation) and less code for controller (which is always good).
In the previous diagram, we can see how the coordinator pattern works: each coordinator manages some screens, it can start children coordinators and it handles flow ending. This leads to the following interface for coordinators
Some implementation requires that stop
must be called when the flow that the coordinator manages ends, so it can notify to its parent coordinator and remove the child coordinator from the array. This must be done or memory leaks can happen.
Navigation Controller: the popping problem
Like stated before, every time a coordinator ends, its parent need to remove it from the childCoordinators
array or memory leaks can occur. The problem arise when using a UINavigationController
. A sub coordinator is pushed to the navigation controller and immediately popped by pressing the back button: there is no way to be notify of this action. At least, not in a straightforward manner. In fact, a simple search leads to quite a few implementations that tries to address this:
- Sublcass
UINavigationController
- Conform to
UINavigationControllerDelegate
- Wrap
UINavigationController
All the previous solutions, in essence, do the same thing: notify to the parent coordinator that the current coordinator should be deallocated. There is another way of doing this, but first we need to look at the reference graph of a common implementation of the pattern.
Here we can see that a strong reference is held for a lower element in the graph and a weak reference is held for an upper one. The AppCoordinator
is held by the AppDelegate
. The rest must be held by the parent coordinator in the array or they will be deallocated after starting it (because the lower element has only a weak reference to its parent). The need to notify to an upper element is to force such upper element to erase the element of the array in order to not generates leak. Enters the …
Inverse Reference Coordinator Pattern
This variation of the pattern reverse the type of reference on the graph
Note that here strong reference are held upward in the graph meanwhile weak reference are downward. The gain of this structure is that if a lower element is deallocated, the reference count of the parent drops one number. If no reference remains, then it is deallocated and the reference counter of the parent of the parent drops one. This can lead to a chain of deallocations upwards which is the idea for the classic implementation. Also, there is no need of an array of children coordinators in each coordinator because the every child coordinator is held by another object. If some cleanup should be done if a lower element is deallocated, then the lower element can call the parent at the deinit
method.
A natural question arise: Who retains the controllers? The answer: UIKit. Controllers must me presented or pushed. When this happens, a strong reference is added to the controller and this retains all the chain to the top. When controllers are dismissed or popped, this strong reference is removed which can lead to the chain of deallocations.
Implementation
An example project can be found here:
In this project, we can see that none of the coordinators have a reference to its child coordinators, because it is not needed, but they have a strong reference to its parent coordinator. Also, the controllers have a strong reference to its coordinators
The project consists of two screens inside a navigation screen. Each screen is associated to coordinator and the second screen is pushed in the navigation screen.
We can see if the second coordinator is deallocated, then it notifies to its parent coordinator. If we run the example project, we can see that this happens if the second controller is popped of the navigation. Also, we can see how structure the code if the coordinator needs to notifies something to the controller (`handleRandom` in the coordinator calls the controller): by a weak reference.
Conclusion
We saw here yet another implementation of the coordinator pattern. The key difference here is that we can manage the popping of navigation controller without modify any other structure using the language itself in our favor. Just like any other pattern, there is no silver bullet and one must adapt it to your needs.
Acknoledgement: thanks to Bastián Véliz who presented this pattern to me.