前文 探讨了 ReSwift,它是基于「单向数据流」的架构方案,来解决 Massive View Controller 灾难。
Soroush Khanlou 写过一篇《8 Patterns to Help You Destroy Massive View Controller》,就多方面来改善工程的维护性和可测试性。
今天要讨论的是其中之一,即在解决「数据流问题」之后,再对视图层的 Navigator 进行解耦,所谓的「Flow Coordinators」。
什么是 Coordinator
Coordinator 是 Soroush Khanlou 在一次演讲中提出的模式,启发自 Application Controller Pattern。
先来看看传统的作法到底存在什么问题。
1 2 3 4 5
| 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
。
实现思路即在 UITableViewDelegate
的代理方法中实现两个 view 之间的跳转。
传统的耦合问题
看似很和谐。
好,现在我们的业务发展了,需要适配 iPad,交互发生了变化,我们打算使用 popover 来显示 detail 信息。
于是,代码又变成了这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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 协议。
1 2 3 4
| protocol Coordinator: class { func start() var childCoordinators: [Coordinator] { get set } }
|
Coordinator 存储了「子 Coordinators」 的引用列表,防止它们被回收,实现相应的列表增减方法。
1 2 3 4 5 6 7 8
| 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
写入启动的代码.
1 2 3 4 5 6 7 8
| 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
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
为例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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
协议。
1 2 3 4 5 6 7 8 9 10
| 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
再进行拓展。
1 2 3 4 5
| protocol Coordinator: class { func start() func start(with option: DeepLinkOption?) var childCoordinators: [Coordinator] { get set } }
|
增加了一个 DeepLinkOption?
类型的参数。这个有什么用呢?
我们可以在 AppDelegate
中针对不同的程序唤起方式都用 Coordinator 进行启动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 类型:
1 2 3 4 5
| enum DeepLinkOption { case login case help case main }
|
我们来实现下 AppCoordinator
中的新 start
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| override func start(with option: DeepLinkOption?) { 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」打法的核心之一。