用 Swift 模仿 Vue + Vuex 进行 iOS 开发(二):Coordinator

本文由 Yison 发表在 ScalaCool 团队博客。html

前文 探讨了 ReSwift,它是基于「单向数据流」的架构方案,来解决 Massive View Controller 灾难。vue

Soroush Khanlou 写过一篇《8 Patterns to Help You Destroy Massive View Controller》,就多方面来改善工程的维护性和可测试性。git

今天要讨论的是其中之一,即在解决「数据流问题」以后,再对视图层的 Navigator 进行解耦,所谓的「Flow Coordinators」。github

什么是 Coordinator

Coordinator 是 Soroush Khanlou 在一次演讲中提出的模式,启发自 Application Controller Patternswift

先来看看传统的做法到底存在什么问题。vim

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)
}
复制代码

再熟悉不过的场景:点击 ListViewController 中的 table 列表元素,以后跳转到具体的 DetailViewControllerapi

实现思路即在 UITableViewDelegate的代理方法中实现两个 view 之间的跳转。架构

传统的耦合问题

看似很和谐。app

好,如今咱们的业务发展了,须要适配 iPad,交互发生了变化,咱们打算使用 popover 来显示 detail 信息。ide

因而,代码又变成了这个样子:

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)
	}
}
复制代码

很快咱们感受到不对劲,通过理性分析,发现如下问题:

  • view controller 之间高耦合
  • ListViewController 没有良好的复用性
  • 过多 if 控制流代码
  • 反作用致使难以测试

Coordinator 如何改进

显然,问题的关键在于「解耦」,看看所谓的 Coordinator 到底起到了什么做用。

先来看看 Coordinator 主要的职责:

  • 为每一个 ViewController 配置一个 Coordinator 对象
  • Coordinator 负责建立配置 ViewController 以及处理视图间的跳转
  • 每一个应用程序至少包含一个 Coordinator,可叫作 AppCoordinator 做为全部 Flow 的启动入口

了解了具体概念以后,咱们用代码来实现一下吧。

不难看出,Coordinator 是一个简单的概念。所以,它并无特别严格的实现标准,不一样的人或 App 架构,在实现细节上也存在差异。

但主流的方式,最可能是这两种:

  • 经过抽象一个 BaseViewController 来内置 Coordinator 对象
  • 经过 protocol 和 delegate 来创建 Coordinator 和 ViewController 之间的联系,前者对后者的「事件方法」进行实现

因为我的更倾向于低耦合的方案,因此接下来咱们会采用第二种方案。

事实上 BaseViewController 在复杂的项目中,也未必是一种优秀的设计,很多文章采用 AOP 的思路进行过改良。

好了,首先咱们定义一个 Coordinator 协议。

protocol Coordinator: class {
    func start()
    var childCoordinators: [Coordinator] { get set }
}
复制代码

Coordinator 存储了「子 Coordinators」 的引用列表,防止它们被回收,实现相应的列表增减方法。

extension Coordinator {
    func addChildCoordinator(childCoordinator: Coordinator) {
        self.childCoordinators.append(childCoordinator)
    }
    func removeChildCoordinator(childCoordinator: Coordinator) {
        self.childCoordinators = self.childCoordinators.filter { $0 !== childCoordinator }
    }
}
复制代码

咱们说过,每一个程序的 Flow 入口是由 AppCoordinator 对象来启动的,在 AppDelegate.swift 写入启动的代码.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
	self.window = UIWindow(frame: UIScreen.main.bounds)
	self.window?.rootViewController = UINavigationController()
	self.appCoordinator = AppCoordinator(with: window?.rootViewController as! UINavigationController)
	self.appCoordinator.start()
        
	return true
}
复制代码

回到咱们以前 ListViewController 的例子,咱们从新梳理下,看看如何结合 Coordinator。假设需求以下:

  • 若是用户未登陆状态,显示登陆视图
  • 若是用户登陆了,则显示主视图列表

定义 AppCoordinator 以下:

final class AppCoordinator: Coordinator {
	fileprivate let navigationController: UINavigationController

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

	override func start() {
		if (isLogined) {
			showList()
		} else {
			showLogin()
		}
	}
}
复制代码

那么如何在 AppCoordinator 中建立和配置 view controller 呢?拿 LoginViewController 为例。

private func showLogin() {
	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()
    }
}
复制代码

再来看看如何定义 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() {
        self.showLogin()
    }

    func showLogin() {
        self.loginViewController.delegate = self
        self.navigationController.show(self.loginViewController, sender: self)
    }
}

extension LoginCoordinator: LoginViewControllerDelegate {
    func didLogin() {
        self.delegate?.didLogin(coordinator: self)
    }
}
复制代码

正如 UIKit 基于 delegate 的设计,咱们靠这种方式真正实现了对 view controller 进行了解耦。

同理 LoginViewController 也存在相应的 LoginViewControllerDelegate 协议。

import UIKit

protocol LoginViewControllerDelegate: class {
    func didLogin()
}

final class LoginViewController: UIViewController {
	weak var delegate:LoginViewControllerDelegate?
	……
}
复制代码

这样,一套基本的 Coordinator 方案就出炉了。固然,目前仍是很是基础的功能子集,咱们彻底能够在这个基础上扩展得更增强大。

适配多入口

显然,一个成熟的 App 会存在多样化的入口。除了咱们一直在讨论的 App 内跳转以外,咱们还会遇到如下的路由问题:

  • Deeplink
  • Push Notifications
  • Force Touch

常见的,咱们极可能须要在手机上点击一个连接以后,直接连接到 app 内部的某个视图,而不是 app 正常打开时显示的主视图。

AndreyPanov 的方案解决了这个问题,咱们须要对 Coordinator 再进行拓展。

protocol Coordinator: class {
    func start()
    func start(with option: DeepLinkOption?)
    var childCoordinators: [Coordinator] { get set }
}
复制代码

增长了一个 DeepLinkOption? 类型的参数。这个有什么用呢?

咱们能够在 AppDelegate 中针对不一样的程序唤起方式都用 Coordinator 进行启动。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  let notification = 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
}
复制代码

利用 buildDeepLink 方法对不一样的入口方式判断输出相应的 flow 类型。

咱们对以前的业务需求进行相应的扩展,假设存在如下三种不一样的 flow 类型:

enum DeepLinkOption {
  case login // 登陆
  case help // 帮助中心
  case main // 主视图
}
复制代码

咱们来实现下 AppCoordinator 中的新 start 方法:

override func start(with option: DeepLinkOption?) {
    //经过 deeplink 启动
    if let option = option {
        switch option {
        case .login: runLoginFlow()
        case .help: runHelpFlow()
        default: childCoordinators.forEach { coordinator in
            coordinator.start(with: option)
        	}
        }
    //默认启动
    } else {
        ……
    }
}
复制代码

总结

本文专门介绍了 Coordinator 模式来对 iOS 开发中的 navigator 进行了深度的解耦。然而当今仍没有权威标准的解决方案,感兴趣的同窗建议去 github 参考下其余更优秀的实践方案。

接下来的第三篇文章计划就 Swift 语言的 extension 语法进行深刻的介绍和分析,它是构建「类 Vue + Vuex」打法的核心之一。

相关文章
相关标签/搜索