阅读 323

Swift路由组件(二)路由的实现

方案的选择

具体一个路由,他真正要做什么事情呢,总结一下应该是这几个逻辑功能:

  1. 通过key来map到一个controller

  2. 实例化一个controller

  3. 解决好传参

  4. 页面跳转

下面讲讲具体代码如何实现每一个逻辑。

一、通过key来map到一个controller

方法有两种:

  1. 通过维护一个plist文件,然后开发的时候添加好路由表。

  2. 在运行的时候通过业务注册,每个业务把key注册到路由里面去,在内存中维护一个路由表。

先说1,这个的做法也简单。定义的plist如下:

image.png

这样运行的时候就可以通过把这个plist读进来,然后就可以做map了。

再说2.这个的简单做法是做一个入口函数,然后各个页面自己把注册的代码添加到这个入口函数里面。比如:

// 启动先初始化好 func onLaunch() { XXRouter.register(to: "native://to_home_page", routerClass: HomeViewController.self)         XXRouter.register(to: "native://to_buy_page", routerClass: BuyViewController.self)   XXRouter.register(to: "http", routerClass: WebViewController.self)   XXRouter.register(to: "https", routerClass: WebViewController.self) } 复制代码

两种方式都需要有一个入口的地方,类似于集中存放。集中存放也是有好处的,方便维护,直观,一眼知道有多少定义,不好的地方就是不方便做组件化。而且组件化还是大家很迫切需要的。

那么这里的需求就是能不能分散定义。

首先我是使用方案2,然后这里我又想到了一种巧妙的方法,先定义一个总的XXRouter类,然后让各个业务自己去extension XXRouter的方法。然后在运行的时候,就可以通过遍历一个类有多少方法,然后再动态的去调用这些方法得到返回值,这样就知道有多少路由定义了。

如下,先定义XXRouter的扩展。

// 添加路由表 extension XXRouter {     @objc func router_WebController() -> XXRouterModel {         return XXRouterModel(to: "http", routerClass: WebController.self)     }     @objc func router_WebController_https() -> XXRouterModel {         return XXRouterModel(to: "https", routerClass: WebController.self)     } } 复制代码

这个多定义的方法的要求如下:

  1. 必须定义为@objc,依赖运行时。

  2. 必须router_开头的函数名,后面可以随便定义名字,建议用路由的类名。如:路由类WebController, 就可以定义函数router_WebController

  3. 必须函数的入参为空。

  4. 这个函数返回值要求是一个XXRouterModel,他的值有: 1)to: 路由名的定义,如:native://course,或者http等唯一的key, 做好分层命名。 2)routerClass: 路由目标类,如:TestClassName.self

运行时遍历和动态调用如下:

private static func configRoutersFromMethodList() -> [String: XXRouterModel] {         var routerList: [String: XXRouterModel] = [:]         var methodCount: UInt32 = 0         let methodList = class_copyMethodList(XXRouter.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 XXRouter.shared.responds(to: selector) {                         if let result = XXRouter.shared.perform(selector).takeUnretainedValue() as? XXRouterModel {                             routerList[result.to] = result                         }                     }                 }             }         }         free(methodList)         return routerList     } 复制代码

  1. 遍历一个类的所有方法

  2. 拿出以router_开头的那些方法。

  3. 动态调用router_开头的方法,获取他的返回值,保存为路由表。

二、实例化一个controller

从路由层面来看也是有两种方法,要么路由自动实例化。要么让实现路由的页面自己实例化。

如,路由里面自动做实例化, 那就是接收一个AnyClass,然后利用NSObject的init(), 方法自动实例化:

private static func getInstance(_ cls: AnyClass) -> XXRoutable? {         if let instance = (cls as? NSObject.Type)?.init(), let routable = (instance as? XXRoutable) {             return routable         }         return nil     } 复制代码

如,让各个页面自己实例话好,再传给路由层。

// 实现路由协议,让自己支持路由跳转。 extension WebController: XXRoutable {     // 返回一个路由协议的实例     static func createInstance(params: [String : Any]) -> XXRoutable {         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

RouterUtil.currentNavigationController() 复制代码

那么push就简单了。

RouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated) 复制代码

具体实现

定义路由协议

通过定义一个路由协议,让实现路由协议的页面都能有跳转的能力。定义一个XXRoutable的协议如下:

/// 路由协议, 需要是AnyObject,其他如struct和enum等不能实现这个协议 public protocol XXRoutable: AnyObject {     /// 路由传参,接收者负责解析自己的参数并返回一个路由实例     static func createInstance(params: [String: Any]) -> XXRoutable     /// 路由逻辑处理     func executeRouter(params: [String: Any], navRootVC: UIViewController?) } 复制代码

  1. 让实现路由的页面,要返回一个实例,实现createInstance方法返回。

  2. 路由要执行的具体跳转逻辑,通过实现executeRouter方法实现。也可以不实现这个,路由组件做了默认的路由跳转。

默认跳转如下:

/// 路由协议的默认实现 public extension XXRoutable {     /// 默认路由跳转     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 XXRoutable {     /// 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 {             RouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)         }     } } 复制代码

注册路由表

前面说过添加路由表通过extension XXRouter方法来实现,然后扩展的方法里面统一返回一个路由表模型。模型定义如下

public class XXRouterModel: NSObject {     /// 路由名,可以任意定义一个唯一的key     /// 如:native://course/detail,这样的一个规则表示本地的某个页面。     /// 或者http,或者https,这两个表示是url网页,直接用webview去响应。     public var to: String = ""     public var routerClass: AnyClass = XXRouterModel.self // 路由目标类          /// 路由模块的路由表定义     /// - Parameters:     ///   - to: 路由名的定义     ///   - routerClass: 路由目标类     public convenience init(to: String, routerClass: AnyClass) {         self.init()         self.to = to         self.routerClass = routerClass     } } 复制代码

校验

路由的定义

通过上面,让一个页面实现路由,那么只需要实现一下路由协议,按路由协议实现对应的方法就能够有路由的功能。再加上一个extension方法去添加一个路由表就行。 整体定义少的话就两个方法,如下:

// 正常的路由定义 extension TestClassName: Routable {     static func createInstance(params: [String : Any]) -> Routable {         return TestClassName()     } } // 多定义一个extension方法 // 添加路由表 extension Router {     @objc func router_TestClassName() -> RouterModel {         return RouterModel(to: "native://testTest", routerClass: TestClassName.self)     } } 复制代码

路由的使用

XXRouter.pushTo(jumpParams: ["to": "native://testTest", "name": "1"]) 复制代码

自动校验

看上面的定义,非常简单,就两个方法。 不过这两个方法的定义都是有要求的。那么假如使用者没有按规则定义怎么办。

  1. 比如,类已经实现了Routable协议,但是忘记加extension的router_xxx方法添加路由表的,会路由失败。

  2. 比如,添加了router_xxx方法添加了路由表,但是忘记实现Routable协议的。

  3. 比如,添加了router_xxx方法添加了路由表,但是添加的router_xxx方法,没有按规定,导致方法不生效。(@objc,函数命名,函数入参,函数返回值等不规范)

  4. 比如,添加的路由表的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? XXRoutable.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)有实现Routable协议,但是没有添加路由表,或者路由表配置没有按规范,请检查:\(cls)。")                 checkList["\(cls)"] = true             }         }         for (key, value) in checkList where value == false {             assert(false, "\(key)有添加路由表,但是没有实现Routable协议,请检查:\(key)。")         }         isCheck = true     } #endif 复制代码

函数做两个事情

  1. 有添加路由表,但是没有实现Routable协议

  2. 有实现Routable协议,但是没有添加路由表,或者路由表配置没有按规范定义。

还缺少一个路由表重复。这个可以在添加路由表的时候加多一个判断就能解决,


作者:清点游玩
链接:https://juejin.cn/post/7032214542528544805

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐