最近在作一个基于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同样在启动时加载,即便有解决方法也显得不够完美。
就像我一开始说的,没有最好的方案,只有最合适的方案。 这其中的利弊,仍是要交给项目交给团队来权衡。
若是你有其余的方式去优化这个方案的话,欢迎留言讨论。