另闢蹊徑--極簡Swifty路由

Ashes and wine.發表於2019-02-14

另闢蹊徑--極簡Swifty路由

1. 前言

在元件化通訊方案的設計之初,儘管我們是純Swift的元件化,我也一直難逃窠臼的想用註冊(無論是註冊協議還是註冊URL)的方式來解決問題,或者採用CTMediatorTarget-Action方式,具體幾種元件化方案的實現與利弊見文章:iOS 元件化 —— 路由設計思路分析??

2. 彎路(經驗)

最開始設計的元件化解決方案,因為作為一個電商專案(才不是這個原因),所以我僅採用了URL註冊的方式,我一直力求的它應該具備的特性如下:

  1. 元件解耦
  2. 可以方便的跳轉到任何已註冊的頁面
  3. 不要硬編碼
  4. 模組(元件)可獲取App生命週期
  5. 模組(元件)對URL的註冊不需要手動呼叫
  6. 可能的話,實現3端一樣的跳轉邏輯
  7. 支援動態下發,如此即支援簡單的熱修復

其實34,已經跳出了元件路由設計的範疇。確切的說應該是模組解耦的範疇。

2.1 實現

我把router設計成單例,目的是保證其持有的["String": func]的字典的唯一確定性,其中func作為閉包形式,傳入引數返回ViewController。那麼註冊環節就顯而易見的為register(_ key: String, value: func),呼叫就會根據key,執行閉包func返回ViewController,以此解決1

在對其中key的設計使用上,因為註冊方與呼叫方都會用到,所以我們將其寫在公共元件內,又因為key會附帶傳一些簡單的值,所以我又加了一個方法對key進行賦值處理操作,目的是為了保證第3條。

在模組解耦問題的處理上,我設計了一個繼承AppDelegate方法的協議,暫稱為AppLifeCycle,同時新增了一些方法用於初始化註冊操作。再又設計了一個指令碼,可以將遵循AppLifeCycle的例項生成一個plist檔案,這樣在App啟動時候,一個方法呼叫就實現所有路由註冊功能,以此解決45

對於第7點,在設計之初因為公司還沒有伺服器端動態下發的功能,所以又加了中介軟體做fallBack處理(當然也都沒用上)。

3. Swifty元件化

雖然原有的路由設計與模組解耦方案已經支援現階段業務需求,但是使用上過於複雜,不夠友好,而且也沒用上多少swift的特性,反而這些實現,如果用Objective-c實現起來,會更方便一些,比如指令碼生成plistOC都可以不需要。

最近有同事在對路由做抽離精簡,僅抽出router部分,主要在介面設計上進行優化。我在看完後對一些功能點提了優化可能,後續一直的交流溝通過程中,突然想到,我可以用Protocol Witness Table來實現這個路由啊!

其原理是: swift會維護一個Protocol Witness Table, 此表會儲存實現了protocol協議的方法的指標地址,當我們呼叫方法時,是通過獲取物件的記憶體地址和方法的位移去查詢的。

所以我們可以用一個協議定義入參,一個協議定義實現,同一個Enum(建議使用的)去實現,即可實現功能。

這種方式類似於target-action,無需註冊,介面約定,還具有其他一些優點:

  1. api介面及其簡單,上手難度0
  2. 介面可以統一在一個庫內,需要的支援庫也變少了
  3. 無硬編碼

那麼如此,我們的路由設計的核心程式碼,如下:

public protocol MediatorTargetType {} // 用於介面定義,約束介面

public protocol MediatorSourceType {  // 用於列舉實現
    var viewController: UIViewController? { get }
}

複製程式碼

target需要遵循的協議就這麼些。

mediator需要遵循的協議與實現:

public protocol SwiftyMediatorType {
    func viewController(of target: MediatorTargetType) -> UIViewController?
}

extension SwiftyMediator: SwiftyMediatorType {
    public func viewController(of target: MediatorTargetType) -> UIViewController? {
        guard let t = target as? MediatorSourceType else {
            print("MEDIATOR WARNINIG: \(target) does not conform to MediatorSourceType")
            return nil
        }
        guard let viewController = t.viewController else { return nil }
        return viewController
    }
}

複製程式碼

以上即是核心程式碼。 通過介面收束,需要傳入MediatorTargetType,嘗試轉換成目標型別MediatorSourceType,以此返回viewController

4. 使用

在使用中,我們仍然需要一個公共的元件庫,對路由目標進行定義。假設這個庫叫MediatorTargets,其內容如下:

public enum ModuleAMediatorType: MediatorTargetType {
    case home(title: String)
    case personal(color: UIColor)
}

複製程式碼

然後在我們寫的模組庫中,此時我們是路由目標的提供方,如3中核心程式碼所示,我們需要 讓ModuleAMediatorType再遵循協議MediatorSourceType,以此支援ModuleAMediatorType返回viewController

import SwiftyMediator
import MediatorTargets

extension ModuleAMediatorType: MediatorSourceType {
    public var viewController: UIViewController? {
        switch self {
        case .home(let title):
            let vc = UIViewController()
            vc.view.backgroundColor = .green 
            vc.title = title
            return vc
            
        case .personal(let color):
            let vc = PresentedViewController()
            vc.view.backgroundColor = color
            vc.title = "Presented"
            return vc
        }
    }
}
複製程式碼

那麼實現方的呼叫,只需要:

import MediatorTargets
import SwiftyMediator

let vc = Mediator.viewController(of: ModuleAMediatorType.home(title: "Home"))

複製程式碼

嗯,就是這麼簡單。

如果只做簡單的模組間通訊,到這是足夠的了, 主要的就是2個協議。

5. 路由化及動態化

當然,有些時候我們需要做一些動態化的路由策略,比如做一下動態路由下發。我也對SwiftyMediator做了一些介面適配,使用方式如下:

  1. 先將需要路由動態化的已遵循MediatorTargetType的協議ModuleAMediatorType,繼續遵循協議MediatorRoutable,並實現協議:
extension ModuleAMediatorType: MediatorRoutable {
    public init?(url: URLConvertible) {
        switch url.pattern {
        case "sy://push":
            self = .push(title: url.queryParameters["title"] ?? "default")
        case "sy://present":
            self = .present(color: UIColor.red)
        default:
            return nil 
        }
    }
}
複製程式碼
  1. 呼叫SwiftyMediatorfunc register(_ targetType: MediatorRoutable.Type),註冊ModuleAMediatorType
  2. 可選:如需要替換某個路由指向,呼叫SwiftyMediatorfunc replace(url: URLConvertible, with replacer: URLConvertible)方法即可
  3. 使用url的方式做路由:Mediator.push("sy://push?title=hahaha")

當需要實現動態化的時候,不可避免的要去註冊,而且要實現協議中的列舉初始化。雖然有些不便,但是在整體的介面收束度上還是挺不錯的。相比較註冊URL的方式來說,這些註冊就少很多了。

6. 模組獲取App生命週期

鑑於目前系統有比較全面的生命週期通知定義,而且不需要在模組中大量註冊url,所以這部分功能目前在考慮是否需要新增。


雖然程式碼很簡單,實現也很簡單,但是跳出慣性思維,再去嘗試同樣需要很多思考。

SwiftyMediator,歡迎star。

其他使用方法見:

demo

參考資料:

WWDC - Protocol Witness Table

swift的witness table

URLNavigator

相關文章