开发中难免有需要制作复杂 UI 的 ViewController,子视图一层套一层,夸张时层层视图都有事件回调,而我们只想在 “C” 中接收回调并处理,能让代码明明白白在业务处理层中展现,让场景易于维护、低耦合、迭代时清晰高效是我们所要达成的最终目的。
这篇文章主要是对结合《一种基于ResponderChain的对象交互方式》的方式与《关于使用 Swift 协议确保类型的思考》的思考与实践。文中涉及到有关 Swift 消息派发机制知识,推荐文章《深入理解 Swift 派发机制》
贯穿全文的例子
假设一个业务场景作为示例:做一个关于天空复杂视图(View)
,天空中有一个太阳(View)
,太阳里有太阳黑子(View)
,这里点击天空或太阳、太阳的Subview都要直接在(ViewController)中做出相应处理。
为了让代码简洁更好说明,点击事件使用简单的 touchesBegan(_:with:)
方法。
代理或闭包回调
闭包相比代理能少很多 XXXViewDelegate 及其实现,但是从架构上看,closure 的处理代码清晰程度必然比不上 delegate,因为创建时机等关系,closure 的代码可能散落各地,而 delegate 回调可以放在一目了然的位置,更加易于debug。
Delegates Closure 交互模式在层级单一,事件较少的业务逻辑里使用完全没毛病。然而在这个场景中,视图越来越复杂后,如例子中处理太阳黑子的事件,甚至点击太阳黑子时也要触发点击太阳的事件就非常难受,我仅仅是想知道我点了一下太阳黑子而已。层级增加随之而来的问题是代码层级过多,显得臃肿维护吃力。
// SunView.swift
protocol SunViewDelegate: class {
/// 从太阳黑子 View 传过来的,继续向 上层传递
func sunView(_ view: SunView, didTapSunspot sunspotId: Int)
/// 点击太阳 View
func sunViewDidTap(_ view: SunView)
}
class SunView: UIView {
weak var delegate: SunViewDelegate?
...
}
// SunspotView.swift
protocol SunspotViewDelegate: class {
/// 太阳黑子点击
func sunspotDidTap(_ view: SunspotView)
}
class SunspotView: UIView {
weak var delegate: SunspotViewDelegate?
...
}
用继承抽象 Delegate
import UIKit
protocol SkyActionType {}
protocol SkyViewDelegate: class {
func homeSubview(_ view: UIView, didTap action: HomeActionType)
}
class SkyBaseView: UIView {
weak var delegate: HomeViewDelegate?
func updateView(model: Model) {
fatalError("[SkyBaseView] Unrealized")
}
}
// 使用
class SunView: SkyBaseView { override func updateView(model: Model) { ... } }
class SunspotView: SkyBaseView { override func updateView(model: Model) { ... } }
...
这是笔者以前使用过的一套模式,这样继承存在很大问题
- 1)所有子视图都存在 Action
- 2)所有视图都需要实现 updateView
- 3)对 Model 要求高
在功能迭代时,很难满足上述几点,即便 updateView 默认实现处不给激进的 fatalError 错误处理,不能保证将来版本的迭代后的 View updateView
方法传值还是 Model,要做额外泛型等处理,或是有的视图根本不存在回调。
把 SkyViewController 的 Subviews 继承一个基类好处是可以减少代码行数,将事件封装到一个代理中,便于修改与删除原有逻辑。但由于层级多、隐藏关键代码的缘故导致代码可读性差,过度依赖也让代码难以重用可维护性较差,局限性大,即便完全理清代码逻辑后,新增需求也因为没有代码提示的缘故容易漏写事件处理。复杂层级的视图这么做也没有解决层层回调代码过与复杂的问题。
我相信大家都码出过这样的代码,虽然本意是想让代码更加优美,但显然还有更好的别的实现方式。
响应链
如何汲取继承的好处,将事件放在一块儿维护?
ResponderChain 就是一个可以实现这样需求的机制。虽说 UIResponder 也是逐级传递,但对开发者来说不必逐级实现传递功能。只需要 extension UIResponder
后添加一个传递方法。其原理不是文章主要内容,不过多赘述,资料非常多。
图中 SunspotView 发送事件(黑色箭头),在响应链后端 SunView 与 SkyView 都可以按需获取 SunspotView 事件(灰色箭头)来实现所需功能。如果在 SunView 中不需要对 SunspotView 的点击事件做处理,开发者则不需要考虑事件在 SunView 的传递实现。
Swift 代码实现:
enum SkyBehavior: String {
case clickSky = "clickSky"
case clickSun = "clickSun"
case clickSunspot = "clickSunspot"
}
extension UIResponder {
@objc func routerEvent(name: String, userInfo: [AnyHashable: Any]?) {
next?.routerEvent(name: name, userInfo: userInfo)
}
}
UIResponder 传递事件实现。
// SkyViewController.swift
class SkyViewController: UIViewController {
...
// 获取所有子视图事件
override func routerEvent(name: String, userInfo: [AnyHashable : Any]?) {
guard let event = SkyBehavior(rawValue: name) else { return }
switch event {
case .clickSky: print("[Sky] click sky")
case .clickSun: print("[Sky] click sun")
case .clickSunspot: print("[Sky] click sun spot")
}
}
}
在天空 VC SkyViewController 中处理事件并停止响应链继续传递。
SkyViewController
× UIWindow
-> UIApplication
-> XCPAppDelegate
由于 Swift 中类的拓展函数是被静态派发的,所以无法被子类继承,我们也不能将代码直接添加到 UIResponder 的声明域中用函数表来派发函数。解决办法只能是利用 NSObject 拓展消息派发函数,在 extension 中加 @objc dynamic
前缀。
如果在点击太阳黑子 SunspotView 的同时也触发点击太阳的事件,只需要在 SunView 中重写方法,为了保证不在这里断链,必须补上super.routerEvent(name: name, userInfo: userInfo)
。
class SunView: UIView {
...
override func routerEvent(name: String, userInfo: [AnyHashable : Any]?) {
super.routerEvent(name: name, userInfo: userInfo)
if name == SkyBehavior.clickSunspot.rawValue {
routerEvent(name: SkyBehavior.clickSun.rawValue, userInfo: nil)
}
}
}
代码SkyExtensionResponderChainPlayground.swift
可以在这个 gist 里找到。
ResponderChain 优化
主要存在的问题:
- 上述基于 ResponderChain 的交互用例功能被分发到了全局,所有继承 UIResponder 的控件全部可以 router 局部的 SkyBehavior。实际业务中,现在 ResponderChain 的实现显然是不合理,当务之急是把发送事件的方法范围缩小,缩小到只有这个SkyViewController和与之相关的视图才拥有这个功能。
- userInfo: [AnyHashable: Any]? 的类型强制性不高,啥都可以往里面传,验证功能时还得跑起来看回调是否到位。
使用泛型协议
喵神在文章开头提到里提到:「相比于 Objective-C 这类“动态”语言,Swift 在类型安全上强制性要高出许多。配合上协议和 associatedtype,更是能做到另一个极致,很多时候可以让我们写出“无脑”的,能通过编译就不会有太大问题的代码。」
Swift 是一门强类型语言,Swifter 更喜欢尽量把东西放在明面上,非工具类业务少些动态,更愿意花时间做一劳永逸的事。Delegate 或 Closure 很大的优点在于不论层级多么复杂,修改功能时会产生大量编译错误,等到开发者处理完编译错误时,功能基本上也就修改完毕,而现在实现的 ResponderChain 并不拥有这个特性,新增回调后忘了修改实现处代码也能编译通过,这只会增加 Debug 的时间。
实际业务中往往存在数据回调,由于 router(event:)
函数是消息派发的,参数必须也是继承 NSObject
的子类,导致 SkyBehavior 枚举不能用 Swift 的关联值这个美妙的特性。利用泛型改造:
public protocol ResponderChainEventType {}
public protocol ResponderChainType {
func router(event: Event) where Event : ResponderChainEventType
}
extension ResponderChainType where Self: UIResponder {
public func router(event: Event) where Event : ResponderChainEventType {
// Responder handler
if let n = next as? SkyViewController {
n.router(event: event)
} else if let n = next as? SunView {
n.router(event: event)
}
// Other hander ...
else {
next?.router(event: event)
}
}
}
extension UIResponder: ResponderChainType {}
改造后 ResponderChainType 对 router 泛型包装,在拓展中这样一来,之前的func routerEvent(name: String, userInfo: [AnyHashable : Any]?)
直接改造成func router
,实现枚举回调事件,其中 ResponderChainEventType 是事件的泛型约束,约束泛型 Event ,操作更加安全可靠。
enum SkyBehavior: ResponderChainEventType {
case clickSky
case clickSun
case clickSunspot(id: Int)
}
// SkyViewController.swift
class SkyViewController: UIViewController {
...
func router(event: Event) where Event : ResponderChainEventType {
guard let e = event as? SkyBehavior else { return }
switch e {
case .clickSky: print("[Sky] click sky")
case .clickSun: print("[Sky] click sun")
case .clickSunspot(let sunspotId): print("[Sky] click sunspot - id: \(sunspotId)")
}
}
}
优化后 userInfo 里的内容被关联到枚举上,Behavior 枚举中也能方便添加回调传值,代码更加优雅直观,作用域明确。代码SkyProtocolResponderChainPlayground.swift
可以在这个 gist 里找到。
视图回调内容强制性比之前更强,比如在删除太阳黑子的 id 回调后,搞定各处的编译错误后,功能也已经搞定了。
美中不足
用于处理事件的 UIResponder
(如 SkyViewController)必须在 extension ResponderChainType
的router
的注释// Responder handler
与// Other hander ...
之间部分中声明。
extension ResponderChainType where Self: UIResponder {
public func router(event: Event) where Event : ResponderChainEventType {
// Responder handler
if let n = next as? SkyViewController {
n.router(event: event)
}
...
}
}
在当前版本 Swift 4.0 的派发机制下,协议的 extension 都会使用直接派发,View 继承于 UIResponder,ResponderChainType 的拓展下,并不能确定 next
的具体类型,只能多写几行将传递的 UIResponder 定位到我们需要的那个后调用 router(event:)
方法。这个位置的判断是必要的,但未实现也是不会报错的,容易造成遗漏。
虽然这个实现并不是最完美,但相对直接利用 NSObject 消息派发的实现方式,已经大大降低了使用时的不确定性。
等到 Swift 在今后更新迭代派发方式发生变化,亦或是笔者悟出更完美的解决办法再来更新解决这个不足。也欢迎大神提点。
模拟业务增删改
搞了这么多,无非是想在功能的增删改时更加舒适,在业务逻辑变更时可以少烧些脑细胞。怎么验证基于 ResponderChain 的交互真的好用呢?
实现一个功能越繁琐、代码分布位置越广时,修改时就越容易遗漏,我们可以简单模拟一下并极度细化业务迭代可能发生的事,以此分别对比 Delegate 与 ResponderChain 增删改所需步骤的繁琐程度。
增删:新增太阳耀斑视图 SolarFlareView
,增加点击事件
增和删其实是一样的,哪里加的代码,删的时候就得回哪儿删,所以这里代码只举例增的情况,把增与删放在一块分析。
// ResponderChain
// 1 - 在 Behavior 中添加点击事件
enum SkyBehavior: ResponderChainEventType {
...
case clickSolarFlare
}
// 2 - 创建视图
// 3 - 添加点击事件,发送事件
class SolarFlareView: UIView {
...
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
router(event: SkyBehavior.clickSolarFlare)
}
}
// 4 - 处理事件 case
class SkyViewController: UIViewController {
...
func router(event: Event) where Event : ResponderChainEventType {
guard let e = event as? SkyBehavior else { return }
switch e {
...
case .clickSolarFlare: print("[Sky] click solar flare")
}
}
}
// Delegates
// 1 - 添加视图 Delegate 声明
protocol SolarFlareViewDelegate: class {
func solarFlareViewDidTap(_ view: SolarFlareView)
}
// 2 - 创建视图
// 3 - 视图内声明 weak var delegate
// 4 - 添加点击事件,发送事件
class SolarFlareView: UIView {
weak var delegate: SolarFlareViewDelegate?
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
delegate?. solarFlareViewDidTap(self)
}
}
// 5 - 回调点击事件到 SunView 中
class SunView: UIView {
let solarFlare = SolarFlareView(frame: .solarFlare)
override init(frame: CGRect) {
super.init(frame: frame)
...
spotView.delegate = self
}
...
}
// 6 - 回调 太阳耀斑视图
protocol SunViewDelegate: class {
...
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView)
}
// 7 - 实现 SolarFlareViewDelegate 继续传递
extension SunView: SolarFlareViewDelegate {
func solarFlareViewDidTap(_ view: SolarFlareView) {
delegate?. sunView(self, didTapSolarFlare: view)
}
}
// 8 - 处理事件
extension SkyViewController: SunViewDelegate {
...
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView) {
print("[Sky] click solar flare")
}
}
基于 ResponderChain 交互用了 4 步,而用 Delegates 居然达到 8 步之多,其中 Delegates 最繁琐的流程在太阳视图 SunView 中,相比前者不但步骤多,代码也分布在各个文件视图中,删除也需要翻遍各个文件。
2)改:修改太阳耀斑视图,在点击SolarFlareView
事件中回调 id (注释老的实现便于对比)
// ResponderChain
// 1 - 修改事件,关联回调
enum SkyBehavior: ResponderChainEventType {
...
// case clickSolarFlare
case clickSolarFlare(id: Int)
}
// 2 - 修改回调传递
class SolarFlareView: UIView {
...
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
// router(event: SkyBehavior.clickSolarFlare)
router(event: SkyBehavior.clickSolarFlare(id: 1))
}
}
// 3 - 修改处理事件 case 获得 id
class SkyViewController: UIViewController {
...
func router(event: Event) where Event : ResponderChainEventType {
guard let e = event as? SkyBehavior else { return }
switch e {
...
// case .clickSolarFlare: print("[Sky] click solar flare")
case .clickSolarFlare(let id): print("[Sky] click solar flare, id: \(id)")
}
}
}
// Delegates
// 1 - 修改代理声明
protocol SolarFlareViewDelegate: class {
// func solarFlareViewDidTap(_ view: SolarFlareView)
func solarFlareView(_ view: SolarFlareView didTap id: Int)
}
// 2 - 修改回调传递
class SolarFlareView: UIView {
weak var delegate: SolarFlareViewDelegate?
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
// delegate?. solarFlareViewDidTap(self)
delegate?. solarFlareView(self, didTap id: 1)
}
}
// 3 - SunView 中修改代理
protocol SunViewDelegate: class {
...
// func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView)
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView, solarFlareId: Int)
}
// 4 - SunView 中修改调用
extension SunView: SolarFlareViewDelegate {
// func solarFlareViewDidTap(_ view: SolarFlareView) {
func solarFlareView(_ view: SolarFlareView didTap id: Int)
// delegate?. sunView(self, didTapSolarFlare: view)
delegate?.sunView(self, didTapSolarFlare: view, solarFlareId: id)
}
}
// 5 - 修改处理事件处
extension SkyViewController: SunViewDelegate {
...
// func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView) {
// print("[Sky] click solar flare")
// }
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView, solarFlareId: Int) {
print("[Sky] click solar flare, id: \(solarFlareId)")
}
}
基于 ResponderChain 交互用了 3 步,Delegates 用了 5 步,主要区别也是在于 SunView 中的修改,多了几个传递步骤。
从业务的增加上对比来说,使用基于 ResponderChain 的交互后增删改迭代功能的步骤都会变少,单独添置内容时也少有影响到别的模块,代码耦合度降低,更为清爽。
总结
视图多事件回调处理方案
- Subviews 视图层级只有一层、子视图回调方法较少时 用
Delegates
或Closure
- Subviews 视图层级只有一层、子视图较多(如3个以上认为已经非常繁琐) 用
ResponderChain
- Subviews 视图层级超过一层 用
ResponderChain
- 不要滥用继承