ahmedk92 / Blog

My blog, in Github Issues.
https://ahmedk92.github.io/Blog/
18 stars 4 forks source link

Covert Coordinators #19

Open ahmedk92 opened 4 years ago

ahmedk92 commented 4 years ago

A coordinator is an architectural component that rose into fame the last couple of years or so. It primarily solves the problem of tight coupling between view controllers regarding their presentation and dismissal. You can learn more about it here.

To summarize, coordinators solve the tight coupling problem by extracting destination view controller creation and navigation implementation details from a source view controller to a coordinator object that manages that flow. So, let's have a simple example.

We have an app that is composed of a view controller (let's call it MainViewController) that is embedded in a UINavigationController, and has a button that pushes a SettingsViewController when tapped.

Coordinators avoid doing this:

class MainViewController {
    @objc func showSettingsButtonTapped(_ sender: Any?) {
        let settingsVC = SettingsViewController()
        navigationController?.pushViewController(settingsVC, animated: true)
    }
}

And do this instead:

class MainViewController {
    weak var coordinator: Coordinator?
    @objc func showSettingsButtonTapped(_ sender: Any?) {
        coordinator?.showSettings()
    }
}

Without writing it explicitly, the code for creating and pushing SettingsViewController went to the coordinator's showSettings().

Covert Patterns

The goal of this article is doing the above exactly without explicitly spelling out "Coordinator". But let me first show what value I find in doing that.

I like to think of a pattern as a form that code evolves into while seeking a set of goals and respecting a set of principles. I find elegance in code developing healthy patterns without expanding the code's vocabulary as possible. Can we achieve the same goals without the "Coordinator" word (or any equivalent)? Let's see.

Let's start with the above code snippet:

class MainViewController {
    weak var coordinator: Coordinator?
    @objc func showSettingsButtonTapped(_ sender: Any?) {
        coordinator?.showSettings()
    }
}

Let's replace the coordinator dependency with a closure:

class MainViewController {
    var showSettings: (() -> Void)?
    @objc func showSettingsButtonTapped(_ sender: Any?) {
        showSettings?()
    }
}

Good. Now, we need to know how the closure is passed and where it's implemented. Coordinators often rely on a container view controller, commonly a UINavigationController. Why not directly use a UINavigationController subclass then? Let's try that.

class MainNavigationController: UINavigationController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let mainVC = MainViewController()
        mainVC.showSettings = { [weak self] in
            self?.pushViewController(SettingsViewController(), animated: true)
        }
        setViewControllers([mainVC], animated: false)
    }
}

Here we subclassed our root navigation controller, and supplied MainViewController's showSettings() implementation. I find this simpler while maintaining the same gains.

Actually I think there's an advantage to this approach over coordinators. If you notice, while using coordinators, MainViewController could call any method it wants from the coordinator property, even if it's irrelevant. Supplying a single closure to call looks much cleaner to me.

Have a look at a sample project where this is implemented. The sample also shows the case of a child flow that would be implemented by so called "child coorindators".

Final Word

As you see, this was an opinionated article. We don't have to agree on this. Feel free to leave your feedback. Thanks for reading.

Yoloabdo commented 4 years ago

I like this, but what if we move the responsibility of this to viewmodel, I think it should know where it's going, since it's asking for a particular path aka URL if we're on web for example, it's the same approach to me but instead of declaring a function name you're passing a closure, not a big difference, so I'd prefer readability

ahmedk92 commented 4 years ago

Thanks.

what if we move the responsibility of this to viewmodel, I think it should know where it's going

It does know where it's going (albeit nominally at best) and what logic-related data is needed for such navigation. But should it do more? like fully instantiate the destination view controller and dictating how it should be presented? You may try and see how it fares, but I don't go with that.

And this is a good opportunity to state some personal architecture principles and explain how I like to think about view models (and presenters in MVP; or whatever qualifies as Logic Controllers).

Principles

  1. Don't fight the API.
  2. Maintain a low surprise factor.

1. Don't fight the API

No matter what, we eventually have to show some view controllers. So, be it a view model, a dedicated coordinator, or a router, all will have to have a reference of an already shown view controller, and the ability to instantiate another view controller and know the UIKit-specific details about its presentation. Fighting this, in addition to being more work, is hard to enforce without hard code reviews, which leads us to the second principle:

2. Maintain a low surprise factor

While not being an absolute merit of code, accessibility to less experienced developers is costly for me to lose/risk. Even for experienced developers, MVC is still king. Interaction between view controllers is expected, putting ad-hoc layers between them is surprising. Such interaction may involve a view controller being a delegate to another view controller, or a view controller showing another view controller. This leads us to my last point:

How I like to think about logic controllers

I like to think about logic controllers as an implementation detail of a view controller. That is, once a view controller receives an event, it reports that as-is to the logic controller. In this system too, the view controller is ready to do whatever the logic controller tells it to do with minimal info as possible. I like logic controllers to only know about their view controllers and no other view controller (or logic controller) even if they're aware of their existence (like we said above: nominally at best). We can use a brain analogy to describe that system. My brain knows about other people; but can only tell my body to shake hands with other people. My brain also doesn't interact directly with other people's brains, it can only tell my body to speak to them. Logic controllers are the brains, and view controllers are the bodies.

So, with all this in mind, you can see the reason behind leaving navigation to view controllers, while making use of closures to reach a middle ground between Apple's MVC's unintended chaos, and alien coordinators.

Nothing stops anybody from viewing navigation as a part of a view controller's logic, and then implement it there. Maybe the view model of a container view controller can be a suitable place to do this, albeit it'll involve having UIViewController objects which may make unit testing view models (a core purpose of using view models) harder.

So, you can try and see. Happy to hear about your results!