上一篇请看这里: Swift路由组件(一)使用路由的目的和实现思想
这个为本人原创,转载请注明出处juejin.cn/post/703221…
方案的选择
具体一个路由,他真正要做什么事情呢,总结一下应该是这几个逻辑功能:
- 通过key来map到一个controller
- 实例化一个controller
- 解决好传参
- 页面跳转
下面讲讲具体代码如何实现每一个逻辑。
一、通过key来map到一个controller
方法有两种:
- 通过维护一个plist文件,然后开发的时候添加好路由表。
- 在运行的时候通过业务注册,每个业务把key注册到路由里面去,在内存中维护一个路由表。
先说1,这个的做法也简单。定义的plist如下:
这样运行的时候就可以通过把这个plist读进来,然后就可以做map了。
再说2.这个的简单做法是做一个入口函数,然后各个页面自己把注册的代码添加到这个入口函数里面。比如:
// 启动先初始化好
func onLaunch() {
YYRouter.register(to: "native://to_home_page", routerClass: HomeViewController.self)
YYRouter.register(to: "native://to_buy_page", routerClass: BuyViewController.self)
YYRouter.register(to: "http", routerClass: WebViewController.self)
YYRouter.register(to: "https", routerClass: WebViewController.self)
}
两种方式都需要有一个入口的地方,类似于集中存放。集中存放也是有好处的,方便维护,直观,一眼知道有多少定义,不好的地方就是不方便做组件化。而且组件化还是大家很迫切需要的。
那么这里的需求
就是能不能分散定义。
首先我是使用方案2,然后这里我又想到了一种巧妙的方法,先定义一个总的YYRouter类,然后让各个业务自己去extension YYRouter的方法。然后在运行的时候,就可以通过遍历一个类有多少方法,然后再动态的去调用这些方法得到返回值,这样就知道有多少路由定义了。
如下,先定义YYRouter的扩展。
// 添加路由表
extension YYRouter {
@objc func router_WebController() -> YYRouterModel {
return YYRouterModel(to: "http", routerClass: WebController.self)
}
@objc func router_WebController_https() -> YYRouterModel {
return YYRouterModel(to: "https", routerClass: WebController.self)
}
}
这个多定义的方法的要求如下:
-
必须定义为@objc,依赖运行时。
-
必须router_开头的函数名,后面可以随便定义名字,建议用路由的类名。如:路由类WebController, 就可以定义函数router_WebController
-
必须函数的入参为空。
-
这个函数返回值要求是一个YYRouterModel,他的值有: 1)to: 路由名的定义,如:native://course,或者http等唯一的key, 做好分层命名。 2)routerClass: 路由目标类,如:TestClassName.self
运行时遍历和动态调用如下:
private static func configRoutersFromMethodList() -> [String: YYRouterModel] {
var routerList: [String: YYRouterModel] = [:]
var methodCount: UInt32 = 0
let methodList = class_copyMethodList(YYRouter.self, &methodCount)
if let methodList = methodList, methodCount > 0 {
for i in 0..<Int(methodCount) {
let selName = sel_getName(method_getName(methodList[i]))
if let methodName = String(cString: selName, encoding: .utf8),
methodName.hasPrefix("router_") {
let selector: Selector = NSSelectorFromString(methodName)
if YYRouter.shared.responds(to: selector) {
if let result = YYRouter.shared.perform(selector).takeUnretainedValue() as? YYRouterModel {
routerList[result.to] = result
}
}
}
}
}
free(methodList)
return routerList
}
- 遍历一个类的所有方法
- 拿出以router_开头的那些方法。
- 动态调用router_开头的方法,获取他的返回值,保存为路由表。
二、实例化一个controller
从路由层面来看也是有两种方法,要么路由自动实例化。要么让实现路由的页面自己实例化。
如,路由里面自动做实例化, 那就是接收一个AnyClass,然后利用NSObject的init(), 方法自动实例化:
private static func getInstance(_ cls: AnyClass) -> YYRoutable? {
if let instance = (cls as? NSObject.Type)?.init(), let routable = (instance as? YYRoutable) {
return routable
}
return nil
}
如,让各个页面自己实例话好,再传给路由层。
// 实现路由协议,让自己支持路由跳转。
extension WebController: YYRoutable {
// 返回一个路由协议的实例
static func createInstance(params: [String : Any]) -> YYRoutable {
let vc = WebController()
vc.url = params["to"] as? String ?? "" // 路由传参数
vc.value = params["value"] as? String ?? "" // 路由传参数
return vc
}
}
这里两种方式都可以,我选择了后面这种,让业务自己实例化好再给路由层,感觉合理些。包括他还可以处理好参数再给回来。
解决好传参
通过上面的方案就能把参数都搞定了。
页面跳转
这个就更简单了。无非就是拿到navigationController,然后push。为了方便,我这里写了一个全局的获取navigationController来做push。看代码
/// 当前的导航控制器
public static func currentNavigationController() -> UINavigationController? {
return currentController().navigationController
}
public static func currentController() -> UIViewController {
if let root = delegate().window??.rootViewController {
return getCurrent(controller: root)
} else {
print("异常问题, 还没有rootVC不应该调用")
assert(false, "异常问题, 还没有rootVC不应该调用")
return UIViewController()
}
}
/// 通过递归拿到当前显示的UIViewController
public static func getCurrent(controller: UIViewController) -> UIViewController {
if controller is UINavigationController {
let naviController = controller as! UINavigationController
return getCurrent(controller: naviController.viewControllers.last!)
} else if controller is UITabBarController {
let tabbarController = controller as! UITabBarController
return getCurrent(controller: tabbarController.selectedViewController!)
} else if controller.presentedViewController != nil {
return getCurrent(controller: controller.presentedViewController!)
} else {
return controller
}
}
有以上的代码,可以这样获取一个全局的navigationController
YYRouterUtil.currentNavigationController()
那么push就简单了。
YYRouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)
具体实现
定义路由协议
通过定义一个路由协议,让实现路由协议的页面都能有跳转的能力。定义一个YYRoutable的协议如下:
/// 路由协议, 需要是AnyObject,其他如struct和enum等不能实现这个协议
public protocol YYRoutable: AnyObject {
/// 路由传参,接收者负责解析自己的参数并返回一个路由实例
static func createInstance(params: [String: Any]) -> YYRoutable
/// 路由逻辑处理
func executeRouter(params: [String: Any], navRootVC: UIViewController?)
}
- 让实现路由的页面,要返回一个实例,实现createInstance方法返回。
- 路由要执行的具体跳转逻辑,通过实现executeRouter方法实现。也可以不实现这个,路由组件做了默认的路由跳转。
默认跳转如下:
/// 路由协议的默认实现
public extension YYRoutable {
/// 默认路由跳转
func executeRouter(params: [String: Any] = [:], navRootVC: UIViewController? = nil) {
guard let controller = self as? UIViewController else {
assert(false, "默认路由跳转,需要routable继承UIViewController")
return
}
defaultPush(to: controller, params: params, navRootVC: navRootVC)
}
}
/// 路由里面的私有方法
public extension YYRoutable {
/// native://my?userId=1&token=jdfsakbfjkafbf
///
/// - Parameters:
/// - controller: 跳转VC
/// - params: 额外参数
/// - navRootVC 有的时候不需要取currentVC
func defaultPush(to controller: UIViewController, params: [String: Any] = [:], navRootVC: UIViewController? = nil) {
let animated = (params["animated"] as? Bool) ?? true
if navRootVC?.navigationController != nil {
navRootVC?.navigationController?.pushViewController(controller, animated: animated)
} else {
YYRouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)
}
}
}
注册路由表
前面说过添加路由表通过extension YYRouter方法来实现,然后扩展的方法里面统一返回一个路由表模型。模型定义如下
public class YYRouterModel: NSObject {
/// 路由名,可以任意定义一个唯一的key
/// 如:native://course/detail,这样的一个规则表示本地的某个页面。
/// 或者http,或者https,这两个表示是url网页,直接用webview去响应。
public var to: String = ""
public var routerClass: AnyClass = YYRouterModel.self // 路由目标类
/// 路由模块的路由表定义
/// - Parameters:
/// - to: 路由名的定义
/// - routerClass: 路由目标类
public convenience init(to: String, routerClass: AnyClass) {
self.init()
self.to = to
self.routerClass = routerClass
}
}
校验
路由的定义
通过上面,让一个页面实现路由,那么只需要实现一下路由协议,按路由协议实现对应的方法就能够有路由的功能。再加上一个extension方法去添加一个路由表就行。 整体定义少的话就两个方法,如下:
// 正常的路由定义
extension TestClassName: YYRoutable {
static func createInstance(params: [String : Any]) -> YYRoutable {
return TestClassName()
}
}
// 多定义一个extension方法
// 添加路由表
extension YYRouter {
@objc func router_TestClassName() -> YYRouterModel {
return YYRouterModel(to: "native://testTest", routerClass: TestClassName.self)
}
}
路由的使用
YYRouter.pushTo(jumpParams: ["to": "native://testTest", "name": "1"])
自动校验
看上面的定义,非常简单,就两个方法。 不过这两个方法的定义都是有要求的。那么假如使用者没有按规则定义怎么办。
- 比如,类已经实现了YYRoutable协议,但是忘记加extension的router_xxx方法添加路由表的,会路由失败。
- 比如,添加了router_xxx方法添加了路由表,但是忘记实现YYRoutable协议的。
- 比如,添加了router_xxx方法添加了路由表,但是添加的router_xxx方法,没有按规定,导致方法不生效。(@objc,函数命名,函数入参,函数返回值等不规范)
- 比如,添加的路由表的key重复了,怎么办。
为了解决这些遗漏的问题,我写了一个自动校验的函数。通过路由初始化的时候,把上面的问题全部检查一遍,有问题直接中断,然后控制住这函数只在Debug里面才运行就行,不影响线上。函数如下:
#if DEBUG
/// 自动检查所有的路由设置是否符合规范,当发现不符合规范的路由设置,直接中断
private static func checkRoutableClassesSettingIsConform() {
guard !isCheck else { return } // 只检查一次
let expectedClassCount = objc_getClassList(nil, 0)
let allClasses = UnsafeMutablePointer<AnyClass>.allocate(capacity: Int(expectedClassCount))
let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses)
let actualClassCount: Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)
for i in 0 ..< actualClassCount {
let currentClass: AnyClass = allClasses[Int(i)]
if (class_getInstanceMethod(currentClass, NSSelectorFromString("methodSignatureForSelector:")) != nil),
(class_getInstanceMethod(currentClass, NSSelectorFromString("doesNotRecognizeSelector:")) != nil),
let cls = currentClass as? YYRoutable.Type {
var isSet = checkList["\(cls)"]
if isSet == nil {
var curCls: AnyClass = cls as AnyClass
// 只要有一个父亲添加了路由表,就表示ok,因为路由那边是不允许子类实现路由协议的,子类只能继承,只能override,或者换别的类去实现路由
while let superCls = curCls.superclass() {
if checkList["\(superCls)"] != nil {
isSet = true
break
}
curCls = superCls
}
}
assert(isSet != nil, "\(cls)有实现YYRoutable协议,但是没有添加路由表,或者路由表配置没有按规范,请检查:\(cls)。")
checkList["\(cls)"] = true
}
}
for (key, value) in checkList where value == false {
assert(false, "\(key)有添加路由表,但是没有实现YYRoutable协议,请检查:\(key)。")
}
isCheck = true
}
#endif
函数做两个事情
- 有添加路由表,但是没有实现YYRoutable协议
- 有实现YYRoutable协议,但是没有添加路由表,或者路由表配置没有按规范定义。
还缺少一个路由表重复。这个可以在添加路由表的时候加多一个判断就能解决,
具体看源码:YYRouter
希望点Star支持。