This post was posted by Yison on the ScalaCool team blog.

The previous article explored ReSwift, which is based on a “one-way data flow” architecture to solve the Massive View Controller disaster.

Soroush Khanlou wrote 8 Patterns to Help You Destroy Massive View Controller to improve the maintainability and testability of projects in many ways.

Today we are going to talk about one of them, that is, after solving the “data Flow problem”, the so-called “Flow Coordinators” decouple the Navigator on the viewing layer.

What is the Coordinator

Coordinator is a model proposed by Soroush Khanlou ina speech, which is inspired by the Application Controller Pattern.

Let’s take a look at what’s wrong with the traditional approach.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
	let item = self.dataSource[indexPath.row]
	let vc = DetailViewController(item.id)
	self.navigationController.pushViewController(vc, animated: true, completion: nil)}Copy the code

A familiar scenario: Click on the Table list element in ListViewController, then jump to the specific DetailViewController.

The idea is to jump between two views in UITableViewDelegate’s proxy method.

The traditional coupling problem

It seems very harmonious.

Ok, now that our business has grown and we need to adapt to the iPad, the interaction has changed, and we’re going to use Popover to display the detail.

Again, the code looks like this:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
	let item = self.dataSource[indexPath.row]
	let vc = DetailViewController(item.id)
	if (! Device.isIPad()) {
		self.navigationController.pushViewController(vc, animated: true, completion: nil)}else {
		var nc = UINavigationController(rootViewController: vc)
		nc.modalPresentationStyle = UIModalPresentationStyle.Popover
		var popover = nc.popoverPresentationController
		popoverContent.preferredContentSize = CGSizeMake(500.600)
		popover.delegate = self
		popover.sourceView = self.view
		popover.sourceRect = CGRectMake(100.100.0.0)
		presentViewController(nc, animated: true, completion: nil)}}Copy the code

Soon we sensed something was wrong, and after rational analysis, we found the following problems:

  • High coupling between View controllers
  • ListViewController is not very reusable
  • Too much if control flow code
  • Side effects make it difficult to test

How coordinators can be improved

Clearly, the key to the problem is “decoupling”, to see what role the so-called Coordinator actually plays.

Coordinator’s main responsibilities:

  • Configure a Coordinator object for each ViewController
  • Coordinators are responsible for creating and configuring viewControllers and handling the jump between views
  • Each application has at least one Coordinator, which can be called an AppCoordinator, to start all flows

Now that we know the concept, let’s implement it in code.

Coordinator is a simple concept. Therefore, it does not have particularly strict implementation standards, different people or App architecture, there are differences in implementation details.

But the mainstream approach, at most, is these two:

  • Built-in Coordinator objects by abstracting a BaseViewController
  • Coordinator and ViewController are connected through protocol and delegate. The former implements the event method of the latter

Since individuals prefer the low-coupling solution, we will go with the second solution.

In fact, BaseViewController in complex projects, is not necessarily a good design, many articles using AOP ideas have been improved.

Ok, first let’s define a Coordinator protocol.

protocol Coordinator: class {
    func start(a)
    var childCoordinators: [Coordinator] { get set}}Copy the code

Coordinators store a list of references to “child Coordinators” to prevent them from being recycled and implement the corresponding method of adding and subtracting the list.

extension Coordinator {
    func addChildCoordinator(childCoordinator: Coordinator) {
        self.childCoordinators.append(childCoordinator)
    }
    func removeChildCoordinator(childCoordinator: Coordinator) {
        self.childCoordinators = self.childCoordinators.filter{$0! == childCoordinator } } }Copy the code

As we said, the Flow entry of each program is started by the AppCoordinator object, which writes the startup code to Appdelegate. swift.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
	self.window = UIWindow(frame: UIScreen.main.bounds)
	self.window? .rootViewController =UINavigationController(a)self.appCoordinator = AppCoordinator(with: window? .rootViewControlleras! UINavigationController)
	self.appCoordinator.start()
        
	return true
}
Copy the code

Going back to our previous ListViewController example, let’s reorganize to see how coordinators can be combined. Assume the following requirements:

  • If the user is not logged in, the login view is displayed
  • If the user is logged in, the main view list is displayed

Define the AppCoordinator as follows:

final class AppCoordinator: Coordinator {
	fileprivate let navigationController: UINavigationController

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

	override func start(a) {
		if (isLogined) {
			showList()
		} else {
			showLogin()
		}
	}
}
Copy the code

How do you create and configure a View Controller in AppCoordinator? Take LoginViewController, for example.

private func showLogin(a) {
	let loginCoordinator = LoginCoordinator(navigationController: self.navigationController)
	loginCoordinator.delegate = self
	loginCoordinator.start()
	self.childCoordinators.append(loginCoordinator)
}

extension AppCoordinator: LoginCoordinatorDelegate {
    func didLogin(coordinator: AuthenticationCoordinator) {
        self.removeCoordinator(coordinator: coordinator)
        self.showList()
    }
}
Copy the code

To define a LoginCoordinator:

import UIKit

protocol LoginCoordinatorDelegate: class {
    func didLogin(coordinator: LoginCoordinator)
}

final class LoginCoordinator: Coordinator {

    weak var delegate:LoginCoordinatorDelegate?
    let navigationController: UINavigationController
    let loginViewController: LoginViewController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
        self.loginViewController = LoginViewController()}override func start(a) {
        self.showLogin()
    }

    func showLogin(a) {
        self.loginViewController.delegate = self
        self.navigationController.show(self.loginViewController, sender: self)}}extension LoginCoordinator: LoginViewControllerDelegate {
    func didLogin(a) {
        self.delegate?.didLogin(coordinator: self)}}Copy the code

Just like UIKit’s delegate-based design, we really decouple the View Controller in this way.

Similarly LoginViewController also exist corresponding LoginViewControllerDelegate agreement.

import UIKit

protocol LoginViewControllerDelegate: class {
    func didLogin(a)
}

final class LoginViewController: UIViewController {
	weak var delegate:LoginViewControllerDelegate? ... }Copy the code

Thus, a basic Coordinator scheme is developed. Of course, it’s still a very basic subset of functionality, and we can expand it even more.

Adaptive multiple entry

Obviously, a mature App has multiple entrances. In addition to the in-app jumps we’ve been discussing, we also encounter the following routing problems:

  • Deeplink
  • Push Notifications
  • Force Touch

Most often, we need to click a link on the phone to link directly to a view inside the app, rather than the main view displayed when the app is normally open.

AndreyPanov’s solution solves this problem, and we need to expand the Coordinator.

protocol Coordinator: class {
    func start(a)
    func start(with option: DeepLinkOption?)
    var childCoordinators: [Coordinator] { get set}}Copy the code

Added a DeepLinkOption? Type parameter. What’s the use of this?

Coordinators can be started in an AppDelegate for different application invocation modes.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  letnotification = launchOptions? [.remoteNotification]as? [String: AnyObject]
  let deepLink = buildDeepLink(with: notification)
  self.applicationCoordinator.start(with: deepLink)
  return true
}

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
  let dict = userInfo as? [String: AnyObject]
  let deepLink = buildDeepLink(with: dict)
  self.applicationCoordinator.start(with: deepLink)
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) - >Bool {
  let deepLink = buildDeepLink(with: userActivity)
  self.applicationCoordinator.start(with: deepLink)
  return true
}
Copy the code

Use the buildDeepLink method to determine the corresponding flow type of the output for different entry modes.

We extend the previous business requirements accordingly, assuming the following three different flow types:

enum DeepLinkOption {
  case login / / login
  case help // Help center
  case main / / main view
}
Copy the code

Let’s implement the new start method in AppCoordinator:

override func start(with option: DeepLinkOption?) {
    // Start with Deeplink
    if let option = option {
        switch option {
        case .login: runLoginFlow()
        case .help: runHelpFlow()
        default: childCoordinators.forEach { coordinator in
            coordinator.start(with: option)
        	}
        }
    // Start by default
    } else{... }}Copy the code

conclusion

This article specifically introduces the Coordinator mode to deeply decouple the Navigator in iOS development. However, there is still no authoritative standard solution. Interested students are advised to refer to other better practices on Github.

The following third article plans to provide an in-depth introduction and analysis of the Swift language’s Extension syntax, which is one of the cores of the “Vue + Vuex” style.