本文由 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」打法的核心之一。