b099l3 / DinkleBot-NativeiOS

A Destiny 2 companion app, that shows useful information for my needs. Also a testbed for me learning modern swift.
6 stars 1 forks source link

Learning Topic: Coordinator Pattern #1

Open b099l3 opened 3 years ago

b099l3 commented 3 years ago

Topic: Coordinator Pattern

Coordinator pattern created by Soroush Khanlou from a series of articles and a talk given at NSSpain

Example protocol for the most basic Coordinator:

protocol Coordinator {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }

    func start()
}

This is a good starting point for the protocol but not every project uses this protocol. I will need to assess other example protocols to see why they have changed parts of this.

Why:

This pattern is to:

Here is a simple example,

public class HomeViewController: UIViewController {

    // ... Other code for View Controller

    @IBAction internal func didPressButton(_ sender: AnyObject) {
        // HomeViewController needs to know about AwayViewController and how to navigate to it.
        if let awayViewController = storyboard?.instantiateViewController(withIdentifier: "AwayViewController") {
            navigationController?.pushViewController(awayViewController, animated: true)
        }
    }
}

with the coordinator pattern and delegation this can result in:

// Coordinator implements this delegate
public protocol HomeViewControllerDelegate: class {
  func homeViewControllerDidPressButton(_ viewController: HomeViewController)
}

public class HomeViewController: UIViewController {
    public weak var delegate: HomeViewControllerDelegate?

    // ... Other code for View Controller

    @IBAction internal func didPressButton(_ sender: AnyObject) {
        // HomeViewController using a delegate
        // Removed coupling between HomeViewController and AwayViewController
        // also removed the navigation concerns away from HomeViewController 
        // as the delegate can decide how it should be navigated to, e.g. modal, push, pop, etc
        delegate?.homeViewControllerDidPressButton(self)
    }
}

This is a much cleaner approach as we can reuse the HomeViewController in other places within our app or swap out its coordinator to change the navigation in homeViewControllerDidPressButton

Notes:

This pattern has been around since 2015 so a lot has progressed since then, prepare to find it has evolved.

Some seem to use Delegates for events between Coordinator and ViewControllers others use Closures, need to see why this is the case?

Using closures with the coordinator pattern

That allows to inject callbacks into a view controller rather than a specific coordinator. This has the effect of creating even more loose coupling than having a coordinator object or protocol, allowing us to replace coordinators with something else entirely if we wanted.

This technique of closure injection is nothing new – in fact, Apple even mentioned it in their WWDC 2016 presentation, Improving Existing Apps with Modern Best Practices. However, I think it has its limits: if you try to pass in more than a couple of closures it gets messy, and you should consider using a coordinator object or protocol instead.

Possibly use both?

Why do some of the examples use Factories?

Resources:

Articles:

https://www.raywenderlich.com/books/design-patterns-by-tutorials/v3.0/chapters/23-coordinator-pattern https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-i-376c836e9ba7 https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-ii-b5ab3eb4a74 https://pavlepesic.medium.com/flow-coordination-pattern-5eb60cd220d5 - references Andrey's Coordinator and others

https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps https://wojciechkulik.pl/ios/mvvm-coordinators-rxswift-and-sample-ios-application-with-authentication https://mattwyskiel.com/posts/2016/07/20/protocol-oriented-app-coordinator-swift.html

https://medium.com/devexperts/real-world-example-of-using-coordinator-pattern-in-an-ios-app-d13df10496a5

https://benoitpasquier.com/coordinator-pattern-navigation-back-button-swift/

Videos:

Examples:

https://github.com/AndreyPanov/ApplicationCoordinator

https://github.com/daveneff/Coordinator https://github.com/igorkulman/iOSSampleApp https://github.com/wojciech-kulik/Swift-MVVMC-Demo

https://github.com/hanifsgy/Journal-MVVMCRxSwift https://github.com/behrad-kzm/SpotifyExplorer https://github.com/omerfarukozturk/MovieBox-iOS-MVVMC https://github.com/sunshinejr/Kittygram

SwiftUI https://github.com/rundfunk47/stinsen https://github.com/SwiftUIX/Coordinator

b099l3 commented 3 years ago

Andrey Panov Coordinator

After doing some reading I quite like the approach taken in these articles by Andrey Panov: https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-i-376c836e9ba7 https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-ii-b5ab3eb4a74

He also describes this pattern in a way that it could be used in MVVM, MVC, VIPER, etc, by calling the VM, VC, IPV - "modules"

Sample code here, it also has tests 🤘: https://github.com/AndreyPanov/ApplicationCoordinator

Why should we use it?

The main goal is to have unchained module-to-module responsibility and to build modules completely independently from each other. After this iteration, we can easily reuse them in different flows.

He uses a similar protocol to the one mentioned above but simpler:

protocol Coordinator: class {
    func start()
    func start(with option: DeepLinkOption?) // This is used for deep linking
}

He has extracted the UINavigationController out of the coordinator and into an object called Router (similar to the RayWenderlich approach and others). I like this idea as it has serves the single responsibility principle, and I can see things getting messy if not extracted out.

He has also moved the childCoordinators into a baseCoordinator so that

So this example's coordinator is composed of two protocols, Coordinator & CoordinatorOutput, to allow for optional results after a flow has finished.

protocol CoordinatorOutput {
    var finishFlow: (Item -> Void)? { get set }
}

Terminology

using "show" prefix in coordinator methods when moving to another module, e.g.,

func showItemList()

using "run" prefix in coordinator methods when moving to another flow/coordinator, e.g.,

func runCreationFlow() 

Summary

The architecture elements proposed here are:

I feel this approach has a lot of fo flexibility and looks easy to extend for other cases in navigation flow. I am going to start with this approach and feedback and issues.

Pavle Pesic has blogged about using this pattern How to implement flow coordinator pattern and has some sample code

b099l3 commented 3 years ago

iOS Academy Coordinator

Video: Swift Coordinator Design Pattern (iOS, Xcode 12, 2021) - iOS Design Patterns

Protocol

protocol Coordinator {
    var navigationcontroller: UINavigationController? { get set }

    funceventOccurred(with type: Event)
    func start()
}

protocol Coordinating {
    var coordinator: Coordinator? { get set }
}

enum Event {
    case buttonTapped
}

Summary

b099l3 commented 3 years ago

James Haville Coordinator

Video: MVVM iOS App Part 4: View | Event Countdown App

Set of tutorials on creaditing MVVM architecture with Coordinator

Protocol

protocol Coordinator {
    var childCoordinators: [Coordinator] { get }
    func start()
}

Summary

b099l3 commented 3 years ago

Ray Wenderlich Coordinator

Article: Coordinator Pattern

Sample Code

Protocol

public protocol Coordinator: class {
  var children: [Coordinator] { get set }
  var router: Router { get }

  func present(animated: Bool, onDismissed: (() -> Void)?)
  func dismiss(animated: Bool)
  func presentChild(_ child: Coordinator,
                    animated: Bool,
                    onDismissed: (() -> Void)?)
}

When to use it?

Use this pattern to decouple view controllers from one another. The only component that knows about view controllers directly is the coordinator.

Consequently, view controllers are much more reusable: If you want to create a new flow within your app, you simply create a new coordinator!

Summary