Coordinator Pattern: No, Router: Yes

Coordinator Pattern: No, Router: Yes

Project source code on GitHub

Coordinator Pattern

Coordinator is not a general design pattern to solve general programming problems. It is a combination of Factory and Delegation design patterns to separate creation and navigation operations from other responsibilities of view controllers.

Coordinator patterns was mentioned for the first time by the Soroush Khanlou. As Khanlou says:
“A coordinator is an object that bosses one or more view controllers around. Taking all of the driving logic out of your view controllers, and moving that stuff one layer up is gonna make your life a lot more awesome.”

If you google the Coordinator, you will see that there are different implementations for the Coordinator pattern. Some of them are simpler, some others more complex.

Isolating view controllers from creation and navigation logic is a great idea. It makes the view controllers more single responsible, reusable, and testable. Hence, it is necessary to have a clear approach to achieve that, especially in medium and large-scale projects.

As far as I understood, Coordinators have some problems. To learn about them, read this post. But what I do not like about Coordinator most of all is the fact that it does not follow a simple approach. It is, to some extent, difficult to understand and maintain, at least for me. So the Coordinator pattern has never been my choice.

Solution: Router

Based on Clean Architecture, we have an interactor and a presenter for a typical view controller. Interactor contains all the business login and presenter is a place for writing codes that convert data models to view models to be easy to represent by view controllers. There are some protocols as boundaries between view controller, interactor, and presenter to have polymorphism. I ignore them here for the sake of simplicity.

Router Architecture
Router Architecture

Arrows show data flows, not dependencies.

The main interactor uses some services to fulfill its needs, services like database, networking, etc. and one of these services is routing service. Routing service is responsible for navigating and sending requests from interactor to other view controllers.

Router Protocol

Router protocol is not a specific protocol with a special definition. It acts as a polymorphic interface to make loose coupling between the interact and the router. Its definition depends on the requirements of the interactor. That means interactor sends special kind of requests to the router that require showing other view controllers to be executed. The login request is a simple example of an operation that needs to show another view controller to perform. So the interactor sends the login request to its Router.

App Navigation Architecture
App Navigation Architecture

Router Contents

Each Router has a weak reference to its own view controller. The main router of the app has also a reference to a window to show the root view controller.

Router Responsibilities

It has four responsibilities:

  1. Instantiate a target view controller.
  2. Navigate to the target view controller.
  3. Call a method on the view controller or pass some data to it.
  4. Disappear its view controller.

Instantiation Target View Controller

Normally it is a heavy task that must be outsourced to the target view controller factory.

Navigation and Method Call

Router is the one who knows how to navigate to the target view controller.

Disappear

Router knows also how to disappear its view controller.

Sample Project

AppRouter

The AppRouter code is like this. We instantiate and pass the app window in the app scene delegate’s scene(_ scene:, willConnectTo:, options:) method.

class AppRouter {
    let window: UIWindow?
    
    init(window: UIWindow?) {
        self.window = window
    }
    
    func start() {
        let rootViewController = MainViewControllerFactory().build()
        let rootNavigationController = UINavigationController(rootViewController: rootViewController)
        window?.rootViewController = rootNavigationController
        window?.makeKeyAndVisible()
    }
}

MainRouterProtocol

MainRouterProtocol is the router of the main interactor. The main interactor declares its requirements there.

public protocol MainRouterProtocol {
     func order(message: String, completion: @escaping (Result) -> Void)
 }

MainRouter

MainRotuer conforms to the MainRouterPrototol and has a reference to the main view controller. Note this reference must be weak, otherwise, there will be a strong reference cycle.

class MainRouter: MainRouterProtocol {
    
    weak var viewController: UIViewController?
    
    func order(message: String, completion: @escaping (Result<String, Error>) -> Void) {
        let orderViewController = OrderViewControllerFactory.build()
        orderViewController.text = message
        orderViewController.completion = completion
        
        viewController?.show(orderViewController, sender: self)
    }
}

OrderRouter

OrderRouter is exactly the same as MainRouter but it has also a dismiss method to disappear the Order view controller.

    class OrderRouter: OrderRouterProtocol {
        
        weak var viewController: UIViewController?
        
        func login(completion: @escaping (Bool) -> Void) {
            //
        }
        
        func dismiss() {
            viewController?.navigationController?.popViewController(animated: true)
        }
    }

Benefits

  • View controllers with a higher degree of single responsibility that are more testable and reusable.
  • Not fighting or duplicating the UIKit structure.
  • An easy-to-understand, maintainable and extendable approach.

Finally, it was my solution to the navigation separation problem.
Please leave comments below if you think this is not perfect.
Thanks,

Project source code on GitHub


Posted

in

by