In a project, I am currently working on, I decided to use the Navigation Coordinator pattern. Not so long ago I started with the simplest implementation and decided to improve it to fit my needs. I documented my first attempt.
I started from a single root coordinator that was completely obscure for its client view controllers. It was good for a simple linear navigation, that can be used in a single navigation controller. As soon as I started adding new rules and secondary navigation controllers I understood that it runs out of my control.
First, I decided to add extra “attributes” to the call. It helped to leave it decoupled from my view controllers. Also, it added more flexibility to the navigation. After I implemented a few more screens I understood that the arguments approach became too complex. It happened after the number of possible arguments grew significantly.
Nice things about coordinators
My next decision was to add “child” coordinators to the main, root coordinator.
class RootCoordinator: NavigationCoordinator {
var navigationController: UINavigationController? //1
private var loggedIn = false
private var coordinators: [NavigationCoordinator] = []
func start() { //2
let firstVC = FirstViewController()
firstVC.coordinator = self
navigationController?.pushViewController(firstVC, animated: false)
}
}
- In the code above
RootCoordinator
has thenavigationController
property. This property should be injected by the parent class. This gives the way to tell the coordinator where its parent wants to present the next screen. - Method
start()
creates the first view controller and presents it.
If I wanted my coordinator to be able to present its view controllers modally I would use an approach similar to the one below.
func start() {
let firstVC = FirstViewController()
firstVC.coordinator = self
if let navigationController = parentViewController as? UINavigationController {
navigationController.pushViewController(firstVC, animated: false)
} else {
firstVC.modalPresentationStyle = .fullScreen
parentViewController?.present(firstVC,
animated: true,
completion: nil)
}
}
Next, I’ve added a coordinator for each part of the application that has a more or less linear navigation.
func openLoginScreen() {
let loginCoordinator = LoginCoordinator()
loginCoordinator.rootViewController = navigationController?.topViewController
loginCoordinator.delegate = self
self.coordinators.append(loginCoordinator)
loginCoordinator.start()
}
Each coordinator has a delegate that accepts navigation commands. In my case, each coordinator expects that its delegate is its parent. In a more complex scheme, some additional object may be used as a dedicated delegate.
protocol LoginCoordinatorDelegate: class {
func userLoggedIn()
}
All coordinators implement the base NavigationCoordinator
protocol. It has a few simple methods that allow navigating backwards. Also, it allows to add or remove other child coordinators. The protocol also has the NavigationState
enum. This enum helps to track the current state and to make decisions based on it.
protocol NavigationCoordinator: class {
func start()
func dismissChild(coordinator: NavigationCoordinator)
func movingBack()
}
Each coordinator connected to its child coordinators through a delegate interface. This interface helps to send navigation events back to the parent coordinator.
Not so nice things
And now I want to describe the problem I’m facing. Each coordinator is tightly coupled with its corresponding set of view controllers. Each view controller has access to its coordinator via a dedicated interface.
The use of an interface reduces the coupling but still requires the coordinator to implement specific methods for each view controller.
At the moment I am using this pattern but planning to improve it somehow in the near future.
The inspiration for the possible improvement I got in one of Uncle Bob’s videos. One of the possible improvements that I see is to add an additional data object. View controllers would pass that object to its coordinators. Such object will contain information about the current state, the next state and the required action. This approach still requires having access to all possible navigation states from both, the view controller and the coordinator. I will need to find a way to break the coupling before implementing this pattern.