另闢蹊徑--極簡Swifty路由
1. 前言
在元件化通訊方案的設計之初,儘管我們是純Swift的元件化,我也一直難逃窠臼的想用註冊(無論是註冊協議還是註冊URL)的方式來解決問題,或者採用CTMediator
的Target-Action
方式,具體幾種元件化方案的實現與利弊見文章:iOS 元件化 —— 路由設計思路分析??
2. 彎路(經驗)
最開始設計的元件化解決方案,因為作為一個電商專案(才不是這個原因),所以我僅採用了URL註冊
的方式,我一直力求的它應該具備的特性如下:
- 元件解耦
- 可以方便的跳轉到任何已註冊的頁面
- 不要硬編碼
- 模組(元件)可獲取App生命週期
- 模組(元件)對
URL
的註冊不需要手動呼叫 - 可能的話,實現3端一樣的跳轉邏輯
- 支援動態下發,如此即支援簡單的熱修復
其實3
、4
,已經跳出了元件路由設計的範疇。確切的說應該是模組解耦的範疇。
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
啟動時候,一個方法呼叫就實現所有路由註冊功能,以此解決4
、5
。
對於第7
點,在設計之初因為公司還沒有伺服器端動態下發的功能,所以又加了中介軟體做fallBack
處理(當然也都沒用上)。
3. Swifty元件化
雖然原有的路由設計與模組解耦方案已經支援現階段業務需求,但是使用上過於複雜,不夠友好,而且也沒用上多少swift
的特性,反而這些實現,如果用Objective-c
實現起來,會更方便一些,比如指令碼生成plist
,OC
都可以不需要。
最近有同事在對路由做抽離精簡,僅抽出router
部分,主要在介面設計上進行優化。我在看完後對一些功能點提了優化可能,後續一直的交流溝通過程中,突然想到,我可以用Protocol Witness Table
來實現這個路由啊!
其原理是: swift
會維護一個Protocol Witness Table
, 此表會儲存實現了protocol
協議的方法的指標地址,當我們呼叫方法時,是通過獲取物件的記憶體地址和方法的位移去查詢的。
所以我們可以用一個協議定義入參,一個協議定義實現,同一個Enum
(建議使用的)去實現,即可實現功能。
這種方式類似於target-action
,無需註冊,介面約定,還具有其他一些優點:
- api介面及其簡單,上手難度0
- 介面可以統一在一個庫內,需要的支援庫也變少了
- 無硬編碼
那麼如此,我們的路由設計的核心程式碼,如下:
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
做了一些介面適配,使用方式如下:
- 先將需要路由動態化的已遵循
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
}
}
}
複製程式碼
- 呼叫
SwiftyMediator
的func register(_ targetType: MediatorRoutable.Type)
,註冊ModuleAMediatorType
- 可選:如需要替換某個路由指向,呼叫
SwiftyMediator
的func replace(url: URLConvertible, with replacer: URLConvertible)
方法即可 - 使用
url
的方式做路由:Mediator.push("sy://push?title=hahaha")
當需要實現動態化的時候,不可避免的要去註冊,而且要實現協議中的列舉初始化。雖然有些不便,但是在整體的介面收束度上還是挺不錯的。相比較註冊URL的方式來說,這些註冊就少很多了。
6. 模組獲取App生命週期
鑑於目前系統有比較全面的生命週期通知定義,而且不需要在模組中大量註冊url,所以這部分功能目前在考慮是否需要新增。
雖然程式碼很簡單,實現也很簡單,但是跳出慣性思維,再去嘗試同樣需要很多思考。
SwiftyMediator,歡迎star。
其他使用方法見:
參考資料: