本文由 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 是 Soroush Khanlou 在一次演讲中提出的模式,启发自 Application Controller Pattern。swift
先来看看传统的做法到底存在什么问题。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 列表元素,以后跳转到具体的 DetailViewController
。api
实现思路即在 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)
}
}
复制代码
很快咱们感受到不对劲,通过理性分析,发现如下问题:
显然,问题的关键在于「解耦」,看看所谓的 Coordinator 到底起到了什么做用。
先来看看 Coordinator 主要的职责:
了解了具体概念以后,咱们用代码来实现一下吧。
不难看出,Coordinator 是一个简单的概念。所以,它并无特别严格的实现标准,不一样的人或 App 架构,在实现细节上也存在差异。
但主流的方式,最可能是这两种:
因为我的更倾向于低耦合的方案,因此接下来咱们会采用第二种方案。
事实上 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 内跳转以外,咱们还会遇到如下的路由问题:
常见的,咱们极可能须要在手机上点击一个连接以后,直接连接到 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」打法的核心之一。