另辟蹊径--极简Swifty路由
1. 前言
在组件化通信方案的设计之初,尽管我们是纯Swift的组件化,我也一直难逃窠臼的想用注册(无论是注册协议还是注册URL)的方式来解决问题,或者采用CTMediator
的Target-Action
方式,具体几种组件化方案的实现与利弊见文章:iOS 组件化 —— 路由设计思路分析??
2. 弯路(经验)
最开始设计的组件化解决方案,因为作为一个电商项目(才不是这个原因),所以我仅采用了URL注册
的方式,我一直力求的它应该具备的特性如下:
- 组件解耦
- 可以方便的跳转到任何已注册的页面
- 不要硬编码
- 模块(组件)可获取App生命周期
- 模块(组件)对
URL
的注册不需要手动调用 - 可能的话,实现3端一样的跳转逻辑
- 支持动态下发,如此即支持简单的热修复
其实3
、4
,已经跳出了组件路由设计的范畴。确切的说应该是模块解耦的范畴。
2.1 实现
我把router
设计成单例,目的是保证其持有的["String": func]
的字典的唯一确定性,其中func
作为闭包形式,传入参数返回ViewController
。那么注册环节就显而易见的为register(_ key: String, value: func)
,调用就会根据key
,执行闭包func
返回ViewController
,以此解决1
。
在对其中key
的设计使用上,因为注册方与调用方都会用到,所以我们将其写在公共组件内,又因为key
会附带传一些简单的值,所以我又加了一个方法对key
进行赋值处理操作,目的是为了保证第3
条。
在模块解耦问题的处理上,我设计了一个继承AppDelegate
方法的协议,暂称为AppLifeCycle
,同时添加了一些方法用于初始化注册操作。再又设计了一个脚本,可以将遵循AppLifeCycle
的实例生成一个plist
文件,这样在App
启动时候,一个方法调用就实现所有路由注册功能,以此解决4
、5
。
对于第7
点,在设计之初因为公司还没有服务器端动态下发的功能,所以又加了中间件做fallBack
处理(当然也都没用上)。
3. Swifty组件化
虽然原有的路由设计与模块解耦方案已经支持现阶段业务需求,但是使用上过于复杂,不够友好,而且也没用上多少swift
的特性,反而这些实现,如果用Objective-c
实现起来,会更方便一些,比如脚本生成plist
,OC
都可以不需要。
最近有同事在对路由做抽离精简,仅抽出router
部分,主要在接口设计上进行优化。我在看完后对一些功能点提了优化可能,后续一直的交流沟通过程中,突然想到,我可以用Protocol Witness Table
来实现这个路由啊!
其原理是: swift
会维护一个Protocol Witness Table
, 此表会保存实现了protocol
协议的方法的指针地址,当我们调用方法时,是通过获取对象的内存地址和方法的位移去查找的。
所以我们可以用一个协议定义入参,一个协议定义实现,同一个Enum
(建议使用的)去实现,即可实现功能。
这种方式类似于target-action
,无需注册,接口约定,还具有其他一些优点:
- api接口及其简单,上手难度0
- 接口可以统一在一个库内,需要的支持库也变少了
- 无硬编码
那么如此,我们的路由设计的核心代码,如下:
public protocol MediatorTargetType {} // 用于接口定义,约束接口
public protocol MediatorSourceType { // 用于枚举实现
var viewController: UIViewController? { get }
}
复制代码
target
需要遵循的协议就这么些。
mediator
需要遵循的协议与实现:
public protocol SwiftyMediatorType {
func viewController(of target: MediatorTargetType) -> UIViewController?
}
extension SwiftyMediator: SwiftyMediatorType {
public func viewController(of target: MediatorTargetType) -> UIViewController? {
guard let t = target as? MediatorSourceType else {
print("MEDIATOR WARNINIG: \(target) does not conform to MediatorSourceType")
return nil
}
guard let viewController = t.viewController else { return nil }
return viewController
}
}
复制代码
以上即是核心代码。 通过接口收束,需要传入MediatorTargetType
,尝试转换成目标类型MediatorSourceType
,以此返回viewController
。
4. 使用
在使用中,我们仍然需要一个公共的组件库,对路由目标进行定义。假设这个库叫MediatorTargets
,其内容如下:
public enum ModuleAMediatorType: MediatorTargetType {
case home(title: String)
case personal(color: UIColor)
}
复制代码
然后在我们写的模块库中,此时我们是路由目标的提供方,如3
中核心代码所示,我们需要 让ModuleAMediatorType
再遵循协议MediatorSourceType
,以此支持ModuleAMediatorType
返回viewController
:
import SwiftyMediator
import MediatorTargets
extension ModuleAMediatorType: MediatorSourceType {
public var viewController: UIViewController? {
switch self {
case .home(let title):
let vc = UIViewController()
vc.view.backgroundColor = .green
vc.title = title
return vc
case .personal(let color):
let vc = PresentedViewController()
vc.view.backgroundColor = color
vc.title = "Presented"
return vc
}
}
}
复制代码
那么实现方的调用,只需要:
import MediatorTargets
import SwiftyMediator
let vc = Mediator.viewController(of: ModuleAMediatorType.home(title: "Home"))
复制代码
嗯,就是这么简单。
如果只做简单的模块间通信,到这是足够的了, 主要的就是2个协议。
5. 路由化及动态化
当然,有些时候我们需要做一些动态化的路由策略,比如做一下动态路由下发。我也对SwiftyMediator
做了一些接口适配,使用方式如下:
- 先将需要路由动态化的已遵循
MediatorTargetType
的协议ModuleAMediatorType
,继续遵循协议MediatorRoutable
,并实现协议:
extension ModuleAMediatorType: MediatorRoutable {
public init?(url: URLConvertible) {
switch url.pattern {
case "sy://push":
self = .push(title: url.queryParameters["title"] ?? "default")
case "sy://present":
self = .present(color: UIColor.red)
default:
return nil
}
}
}
复制代码
- 调用
SwiftyMediator
的func register(_ targetType: MediatorRoutable.Type)
,注册ModuleAMediatorType
- 可选:如需要替换某个路由指向,调用
SwiftyMediator
的func replace(url: URLConvertible, with replacer: URLConvertible)
方法即可 - 使用
url
的方式做路由:Mediator.push("sy://push?title=hahaha")
当需要实现动态化的时候,不可避免的要去注册,而且要实现协议中的枚举初始化。虽然有些不便,但是在整体的接口收束度上还是挺不错的。相比较注册URL的方式来说,这些注册就少很多了。
6. 模块获取App生命周期
鉴于目前系统有比较全面的生命周期通知定义,而且不需要在模块中大量注册url,所以这部分功能目前在考虑是否需要添加。
虽然代码很简单,实现也很简单,但是跳出惯性思维,再去尝试同样需要很多思考。
SwiftyMediator,欢迎star。
其他使用方法见:
参考资料: