Recently I started to use the “Navigation Coordinator” pattern in my projects.
This pattern helps to significantly reduce coupling. It takes responsibility for choosing and presenting the right view controller from another view controller.
I borrowed the idea from a course on Lynda.com. I made changes that allow choosing where you want to navigate from the current View controller.
Another place where this concept is well explained is a Swift Talk episode Connecting View Controllers I’m not completely satisfied with this approach and working on the update. I described the following update in my next post about coordinators.
Below is an example of a traditional navigation pattern often used in code examples and simple applications:
Here we have two functions that can be called to present another ViewControllers. In case of ScreenBViewController
, we also inject a dependency.
MainViewController
should know how to create theScreenAViewController
.- It also knows where to present (
self.navigationController
) - And how to present (
pushViewController(viewController, animated: true)
)
With this pattern, the MainViewController
besides to its main function also should know how and where to present the ScreenAViewController
and ScreenBViewController
.
It bloats the code and violates the Single Responsibility Principle. To deal with it we should remove all mentions of other ViewControllers from the MainViewController
. The Navigation Coordinator should make decisions about the controller it should present next.
##Refactoring for Navigation Coordinator
To simplify the example I will create the new controller inside the Navigation Coordinator. In a real project we should move this responsibility to some object factory. Also I will not use protocols.
As a starting point, I used a simple method that allows linear navigation between two View Controllers.
Let’s create a NavigationCoordinator
class and pass it to our MainViewController
from a common parent class.
In the NavigatoinCoordinator
above we made a few steps that will allow to create and present the screen we need.
- We use the enum to store possible navigation states.
- In the initializer, we inject the navigation controller that will present the child view controllers.
- The function
next(arguments: [String: Any]?)
checks the current navigation state to find the ViewContrtoller from which it is called. Then it calls the corresponding navigation method. - In the navigation function, we use a passed argument to find what screen should be created.
Let’s check the new MainViewController
- We use the enum
MainViewNavigation
to store the codes of all possible screens we need to navigate to. - The
NavigationCoordinator
is passed from the common parent and stored as a weak variable. It is very similar to the Delegate pattern. - Now, instead of instantiating a new ViewController we call the method
next
of the navigation coordinator letting it know what kind of screen we want to present (or what event happened, if you prefer) - If we need to inject a dependency, we can do it by passing an additional argument.
- It’s possible to add all kinds of additional arguments to the call.
The updated MainViewController
knows nothing about screens that will be presented. It also doesn’t know where or how these controllers will be presented.
The only external method it has access to is navigationCoordinator.next(arguments:)
. By passing the “screen” and “dependency” arguments it allows NavigationCoordinator to decide what to do next.
It looks like this approach adds a lot of code, but it gives much more benefits in return.
- It reduces code coupling
- It helps to make code that follows the Single Responsibility Principle
- It makes the code testable
Please leave a comment if you know a better way to achieve this functionality or have a question.