Coordinator Pattern - Basic

There are several tutorials on internet explaining the origin and how it works, so I’ll briefly explain why every swift developer has to know the coordinator pattern.

Problem

Remember that the function of the design pattern is to solve a problem that exists? Soroush Khanlou at, a 2015 conference, identified that there is a problem with the screen flow allowing the creation of a coupled app and many stacks of views. His talk is here.

In our app, we don't want one class to know about the other. Much less a massive view controller, as it is not the function of the ViewController to transition the screen.

Solution

A ViewController will not know about the existence of the other, so the coordinator will do the function of communicating with other views and creating transitions, in this way, the application will be more reusable, scalable, and easy to maintain.

Let's go to practice

The structure of the app looks like this:

Screen Shot 2021-04-03 at 19 06 29

The coordinator starts from the moment you open the app, so our SceneDelegate will have a coordinator.

Normally our sceneDelegate starts like this:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let scene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: scene)
        window?.rootViewController = UINavigationController(rootViewController: ViewController())
        window?.makeKeyAndVisible()


    }
}

However, we cannot instantiate our ViewController, so we will need the help of a coordinator. To use it, let's create a protocol in the Coordinator.swift file.


protocol Coordinator {
    func start()
    func coordinate(coordinator: Coordinator)
}

extension Coordinator{
    func coordinate(coordinator: Coordinator){
        coordinator.start()
    }
}

All views that need to call some screen will have to create a coordinator class and adapt to the protocol.

Let's do it in SceneDelegate, we will create a class called AppCoordinator, in SceneDelegateCoordinator.swift.

class AppCoordinator{

    let window: UIWindow

    init(window: UIWindow) {
        self.window = window
    }

}

It will initialize with UIWindow because this class will manage the SceneDelegate flow. Let's associate the protocol.

class AppCoordinator: Coordinator {

    let window: UIWindow

    init(window: UIWindow) {
        self.window = window
    }

    func start() {


    }

}

The func start () will serve to call the view we want.

 func start() {
    let navigationController = UINavigationController()
    window.rootViewController = navigationController
    window.makeKeyAndVisible()

    let initCoordinator = InitViewControllerCoordinator(navigationController: navigationController)

    coordinate(coordinator: initCoordinator)
}

Note that we call the InitViewControllerCoordinator, which has not yet been created, but it is the coordinator for the InitViewController. The Coordinate (coordinator: initCoordinator) calls the start function of the coordinator of the InitViewController.

Our SceneDelegate will look like this with the coordinator.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var coordinator: AppCoordinator?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let scene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: scene)
        coordinator = AppCoordinator(window: window!)
        coordinator?.start()
    }
}

As our SceneDelegate calls the view InitViewController, it will have a coordinator.

Let's create the InitViewController coodinator.

InitViewController.swift

class InitViewControllerCoordinator: Coordinator, InitFlow {

    let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let initViewController = InitViewController()
        navigationController.pushViewController(initViewController, animated: true)

    }

}

The constructor asks for the NavigationController, and we will use func start () {} to initialize the visualization.

We finished our first flow. Now we are going to implement a flow in which the button calls another screen.

InitViewController.swift

Let's create the button UI in a file called UIInitViewController and also a delegate of the button action for the InitViewController:


protocol TouchButtonProtocol: class {

    func sendButton()
}

class UIInitViewController: UIView {

    weak var delegate: TouchButtonProtocol?

    let flowButton: UIButton = {
        let btn = UIButton()
        btn.backgroundColor = .gray
        btn.setTitle("Send", for: .normal)
        btn.addTarget(self, action: #selector(send), for: .touchUpInside)
        btn.translatesAutoresizingMaskIntoConstraints = false

        return btn
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.backgroundColor = .systemBackground
        self.constraintsButton()

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func constraintsButton(){

        self.addSubview(flowButton)


        NSLayoutConstraint.activate([
            self.flowButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            self.flowButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            self.flowButton.heightAnchor.constraint(equalToConstant: 40)

        ])

    }

    @objc func send(_ button: UIButton){
        self.delegate?.sendButton()
    }
}

Our InitViewController:


class InitViewController: UIViewController {

    var uiLayout = UIInitViewController()

    override func loadView() {
        self.view = uiLayout
        self.uiLayout.delegate = self

    }

    override func viewDidLoad() {
        super.viewDidLoad()

    }
}
extension InitViewController: TouchButtonProtocol{
    func sendButton() {

    }
}

This button that we call in the InitViewController, will have the action of calling another view. We would normally use it like this:


extension InitViewController: TouchButtonProtocol{
    func sendButton() {
        let finishViewController = FinishViewController()
        self.navigationController?.pushViewController(finishViewController, animated: true)
    }
}

However, we can improve our code, as the viewController does not coordinate screen flow, and doing so will only increase the amount of code and dependencies.

To resolve this, let's create an initViewController delegate with initViewControllerCoordinator for the coordinator to manage the transition.

protocol InitFlow: class {
    func coordinatorToFinish()
}

class InitViewController: UIViewController {

    var flowCoordinator: InitFlow?

    ...

}
extension InitViewController: TouchButtonProtocol{
    func sendButton() {
        flowCoordinator?.coordinatorToFinish()
    }
}

In the coordinator of this view, we will associate the protocol with the class.


class InitViewControllerCoordinator: Coordinator, InitFlow {

    ...

    func coordinatorToFinish() {
        let finishViewController = FinishViewcControllerCoordinator(navigationController: navigationController)

        coordinate(coordinator: finishViewController)
    }
}

In the func coordinatorToFinish () function, it will call the finishViewController coordinator

FinishViewcControllerCoordinator.swift


class FinishViewcControllerCoordinator: Coordinator{

    let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let finishViewController = FinishViewController()
        navigationController.pushViewController(finishViewController, animated: true)
    }

}

FinishViewController.swift


import Foundation
import UIKit

class FinishViewController: UIViewController{

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .gray

    }

}

Conclusion

Note: Each view controller has its coordinator, which is not an advantage if you have a very large application, but to get an idea of ​​how the coordinator works, the tutorial illustrates its advantages.

The app was made based on this article

The code is on GitHub.

Kapture 2021-04-04 at 02 14 22

Hope this helps!