Swift基於枚舉的路由設計實踐

最近在作一個基於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須要依賴組件去建立界面,這樣形成了模塊間的循環引用。因此,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讓其餘模塊調用便可。

引入子模塊以及Swift的坑

寫完全部子模塊了,咱們只須要在主項目中像下圖這樣引入子模塊,或者使用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
複製代碼

總結

到此爲止,這個方案算是優缺點明顯了,簡單總結下吧。

優勢:

  • 模塊間耦合低。
  • 模塊間的跳轉是可靠的。
  • 能夠適配兼容Objective-C編寫的模塊。

缺點:

  • 仍然沒法徹底避免硬編碼,你須要將枚舉映射路由表中對應的key。
  • 模塊間的通信不夠靈活。
  • 你在維護路由的自己的同時還可能須要每一個子模塊去維護它加載Class的方法。

對於我來講,這個方案上最大的問題在於Swift中的Class不能像Objective-C同樣在啓動時加載,即便有解決方法也顯得不夠完美。

就像我一開始說的,沒有最好的方案,只有最合適的方案。 這其中的利弊,仍是要交給項目交給團隊來權衡。

Demo地址

若是你有其餘的方式去優化這個方案的話,歡迎留言討論。

相關文章
相關標籤/搜索