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

相關文章
相關標籤/搜索