最近在作一個基於Swift的路由設計,其實我在剛開始接觸Swift的時候就以爲Swift的枚舉很適合作路由。比方說相似於下面這樣的寫法:git
Router.pushTo(.user(.profile(userID: "Jake")), from: self, animated: true)
let loginVC = Router.viewControllerWithPath(.user(.login))
複製代碼
枚舉的嵌套設計、容許帶參數,且能夠明確參數是否可選,可讓模塊間調用更爲可靠。那麼要如何實如今如今多人合做和組件化項目的場景中呢。github
固然在這以前我仍是那句話,沒有最好的方案,只有最合適的方案。json
組件化的問題在於組件間的依賴關係,比方說界面須要依賴Router跳轉,而Router須要依賴組件去建立界面,這樣形成了模塊間的循環引用。因此,Router不可以依賴子模塊,咱們須要採用NSClassFromString
的方式建立對象。bash
固然咱們須要用協議去約束這個Class和利用這個Class處理對應的路由路徑操做。協議相似於下面這段代碼,這裏的協議設計也能夠比較容易的去適配Objective-C寫的模塊。組件化
public protocol Routable: NSObject {
static func viewControllerWith(routePath: RouterProtocol.Path) -> UIViewController?
}
複製代碼
得益於Swift強大的枚舉,咱們能夠像這樣聲名咱們的路由路徑優化
public struct RouterProtocol {
public enum Path {
public enum User {
case login
case profile(userID: String)
}
case user(User)
case search
}
}
複製代碼
接下來咱們的Router須要一個路由表文件,這裏我爲了方便就使用了json,能夠根據實際項目和我的習慣選擇其餘文件方式,那麼咱們的路由表文件能夠是這樣的:ui
{
"login":{
"class":"UserModule.LoginViewModel"
},
"userProfile":{
"class":"UserModule.UserViewModel"
},
"search":{
"class":"SearchModule.SearchViewModel"
}
}
複製代碼
如今咱們能夠根據咱們的枚舉和路由表對應的字段,寫出解析了。編碼
實際上能夠將路由表的解析方法利用代理的方式交給主項目解析,由主項目決定,這樣能夠進一步減小耦合。url
public struct RouteTarget {
public let className: String
public let urlString: String?
public init(className: String, urlString: String? = nil) {
self.className = className
self.urlString = urlString
}
}
public protocol RouterDelegate: class {
func routeTargetWithPath(_ path: RouterProtocol.Path) -> RouteTarget?
}
複製代碼
那麼Router獲取界面方法相似於下面的代碼:spa
static public func viewControllerWithPath(_ path: RouterProtocol.Path) -> UIViewController? {
guard let target = Router.delegate?.routeTargetWithPath(path),
let routableClass = NSClassFromString(target.className) as? Routable.Type else {
return nil
}
if let urlString = target.urlString,
let url = routableClass.handleURLString(urlString, routePath: path) {
return SFSafariViewController(url: url)
}
return routableClass.viewControllerWith(routePath: path)
}
複製代碼
這裏我加了個解析路由表中的url字段和在原來的協議RouterProtocol中添加了handleURLString
方法,主要是爲了當路由表中出現url字段時候,跳轉至網頁而不是界面,固然URL的處理方式應該是直接交給實現協議的類處理的。
同理,你還能夠經過定義好比handleMessageWithPath
的方法來實現簡單的模塊間通信,這裏再也不贅述。
目前看來還不錯,得益於枚舉咱們有了可靠的模塊間調用方式,也能比較好的適配Objective-C編寫的模塊。而咱們維護Router只須要添加枚舉項和對應路由表映射。
這裏還能夠再優化下,能夠將RouterProtocol和Router拆分,如果當前模塊不須要調用其餘模塊的話,只須要引入並實現RouterProtocol讓其餘模塊調用便可。
寫完全部子模塊了,咱們只須要在主項目中像下圖這樣引入子模塊,或者使用Cocoapods等組件化方式引入子模塊。
如今,你可能覺得已經結束了,你能夠愉快的在項目中使用路由了。其實並無那麼簡單,比方說你在這時候像下面的代碼利用Router獲取界面其實是不可行的,Router會返回nil,找不到對應的界面。
let loginVC = Router.viewControllerWithPath(.user(.login))
複製代碼
在這以前咱們都理所固然的認爲NSClassFromString
能夠幫助咱們動態的建立相應的Class,事實上由於Swift與Objective-C不一樣,程序啓動時模塊中的Class是沒有被加載的。所以下面這段代碼實際結果爲空
NSClassFromString("UserModule.LoginViewModel") == nil
複製代碼
因此咱們須要對模塊的Class進行「註冊」以後才能使用。簡單來講咱們須要使用一次這個Class,能夠是建立它也能夠只是獲取它。比方說你能夠經過簡單的let _ = LoginViewModel.self
,以後Router就能夠反射對應的Class了。
這裏能夠在各個模塊中定義一個loadClass
的方法,而後在啓動的時候調用每一個子模塊的loadClass
方法。或者,每一個子模塊中用Objective-C建立一個類,在其+load()
方法中調用(這個方法在靜態庫中須要在linker flags中添加force_load
參數)。相似於下面這樣的寫法:
#import "UserModuleRegister.h"
#import <UserModule/UserModule-Swift.h>
@implementation UserModuleRegister
+ (void)load {
[UserModule loadClass];
}
@end
複製代碼
到此爲止,這個方案算是優缺點明顯了,簡單總結下吧。
對於我來講,這個方案上最大的問題在於Swift中的Class不能像Objective-C同樣在啓動時加載,即便有解決方法也顯得不夠完美。
就像我一開始說的,沒有最好的方案,只有最合適的方案。 這其中的利弊,仍是要交給項目交給團隊來權衡。
若是你有其餘的方式去優化這個方案的話,歡迎留言討論。