Swift 复杂视图多事件回调处理方案思考
开发中难免有需要制作复杂 UI 的 ViewController,子视图一层套一层,夸张时层层视图都有事件回调,而我们只想统一接收回调并处理,能让代码明明白白在业务处理层中展现,让场景易于维护、低耦合、迭代时清晰高效是我们所要达成的最终目的。
这片文章主要是对结合《一种基于ResponderChain的对象交互方式》的方式与《关于使用 Swift 协议确保类型的思考》的思考与实践。
贯穿全文的例子
假设一个业务场景作为示例:做一个关于天空复杂视图(View)
,天空中有一个太阳(View)
,太阳里有太阳黑子(View)
,这里点击天空或太阳、太阳的Subview都要直接在(ViewController)中做出相应处理。
![](https://img.haomeiwen.com/i3790620/4eef7dd71e0227a2.png)
为了让代码简洁更好说明,点击事件使用简单的 touchesBegan(_:with:)
方法。
代理或闭包回调
闭包相比代理能少很多 XXXViewDelegate 及其实现,但是从架构上看,closure 的处理代码清晰程度必然比不上 delegate,因为创建时机等关系,closure 的代码可能散落各地,而 delegate 回调可以放在一目了然的位置,更加易于debug。
![](https://img.haomeiwen.com/i3790620/b23e3c5a61c63a99.png)
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
把 SkyViewController 的 Subviews 继承好处是可以减少代码行数,将事件封装到一个代理中,便于修改与删除原有逻辑。但由于层级多、隐藏关键代码的缘故导致代码可读性差,想象一下你的同事或未来的自己回到这里看代码时候的一脸懵逼。过度依赖也让代码难以重用可维护性较差,局限性大,即便完全理清代码逻辑后,新增需求也因为没有代码提示的缘故容易漏写事件处理。
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")
}
}
这是笔者以前使用的一套模式,这样继承存在非常大的问题
- 1)所有子视图都存在 Action
- 2)所有视图都是动态的,需要实现 updateView
在功能迭代时,很难满足上述两点,即便 updateView 默认实现处不给激进的 fatalError 错误处理,也不能保证之后版本的迭代有的 View 不是动态的更新方法,或是有的视图根本不存在回调,导致代码越来越难看,所以继承要慎用(深刻反思)。并且这么做也没有解决层层回调代码过与复杂的问题。
响应链
如何汲取继承的好处,将事件放在一块儿维护?
ResponderChain 就是一个可以实现这样需求的机制。虽说 UIResponder 也是逐级传递,但对开发者来说不必逐级实现传递功能。只需要 extension UIResponder
后添加一个传递方法。其原理不是文章主要内容,不过多赘述,资料非常多。
![](https://img.haomeiwen.com/i3790620/6fd42bf87388af50.png)
图中 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 并不支持在拓展中重写父类方法,只能添加新方法。这里需要曲线处理。解决办法是在 extension 中加 @objc 前缀,弊端是方法中所有属性必须是都是支持OC调用的类型。
如果在点击太阳黑子 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 时间长的开发者都会为这个观点疯狂打 Call, Swift 是一门强类型语言,Swifter 更喜欢尽量把东西放在明面上,非工具类业务少些动态,更愿意花时间做一劳永逸的事。Delegate 或 Closure 很大的优点在于,不论层级多么复杂,修改功能时会产生大量编译错误,等到开发者处理完编译错误时,功能基本上也就修改完毕,而现在实现的 ResponderChain 并不拥有这个特性,新增回调后忘了修改实现处代码也能编译通过,这只会增加 Debug 的时间。
实际业务中往往存在View的数据回调,由于加上@objc
前缀的方法中,参数必须也是NSObject
的继承类,导致 SkyBehavior 枚举不能用 Swift 的关联值这个美妙的特性,因此我们要利用泛型改造回调。
public protocol ResponderChainEventType {}
public protocol ResponderChainType {
func router<Event>(event: Event) where Event : ResponderChainEventType
}
extension ResponderChainType where Self: UIResponder {
public func router<Event>(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<Event>(event: Event) where Event : ResponderChainEventType
,实现枚举回调事件,其中 ResponderChainEventType 是事件的泛型约束,约束泛型 Event ,操作更加安全可靠。
enum SkyBehavior: ResponderChainEventType {
case clickSky
case clickSun
case clickSunspot(id: Int)
}
// SkyViewController.swift
class SkyViewController: UIViewController {
...
func router<Event>(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)")
}
}
}
优化后不用考虑拓展中重写需要添加@objc会产生安全或性能问题,亦不存在任何与OC、runtime相关的影子。userInfo 里的内容被关联到枚举上,Behavior枚举中也能方便添加回调传值,代码更加优雅直观,作用域明确。代码SkyProtocolResponderChainPlayground.swift
可以在这个 gist 里找到。
视图回调内容强制性比之前更强,比如在删除太阳黑子的 id 回调后,搞定各处的编译错误后,功能也已经搞定了。要注意的是用于处理事件的 UIResponder
(比如 SkyViewController)必须在extension ResponderChainType
的router<Event>(:)
的注释// Responder handler
与// Other hander ...
之间的部分中声明。
原因是 Swift 的 protocol extension 在不知道属性具体类型时,只会调用当前类型锁实现的 protocol 内容,如下面这段代码:
class Super {}
protocol P {
associatedtype T
func method(_ num: T)
}
protocol IntP: P {}
extension IntP where Self: Super {
func method<T>(_ num: T) {
print("Super")
}
}
extension Super: IntP {
typealias T = Int
}
class Sub: Super {
func method(_ num: T) {
print("Sub")
}
}
let sub: Super = Sub()
sub.method(2) // Super
(sub as! Sub).method(2) // Sub
虽然声明为 Super 类的 sub 实例是用 class Sub 实现,但调用方法时调的还是 IntP 中的默认实现。
sub.method(2) // Super
只有明确知道 sub 所属 Sub 类型后 sub 才会调用 Sub 类中实现的方法。所以在 router 时获取到 next responder 时,只能将这个UIResponder
类型再转回实际的类型,才能处理并停止链的传递。这里确实容易忘加造成遗漏,笔者现在暂未想到更加强制
的处理办法,如有好的想法务必联系笔者讨论讨论!!
模拟业务增删改
搞了这么多,无非是想在功能的增删改时更加舒适,在业务逻辑变更时可以少烧些脑细胞。怎么验证基于 ResponderChain 的交互真的好用呢?
实现一个功能越繁琐、代码分布位置越广时,修改时就越容易遗漏,我们可以简单模拟一下并极度细化业务迭代可能发生的事,以此分别对比 Delegate 与 ResponderChain 增删改所需步骤的繁琐程度。
![](https://img.haomeiwen.com/i3790620/68ff8d91c051ea7b.png)
增删:新增太阳耀斑视图 SolarFlareView
,增加点击事件
增和删其实是一样的,哪里加的代码,删的时候就得回哪儿删,所以这里代码只举例增的情况,把增与删放在一块分析。
// ResponderChain
// 1 - 在 Behavior 中添加点击事件
enum SkyBehavior: ResponderChainEventType {
...
case clickSolarFlare
}
// 2 - 创建视图
// 3 - 添加点击事件,发送事件
class SolarFlareView: UIView {
...
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
router(event: SkyBehavior.clickSolarFlare)
}
}
// 4 - 处理事件 case
class SkyViewController: UIViewController {
...
func router<Event>(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<UITouch>, 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<UITouch>, with event: UIEvent?) {
// router(event: SkyBehavior.clickSolarFlare)
router(event: SkyBehavior.clickSolarFlare(id: 1))
}
}
// 3 - 修改处理事件 case 获得 id
class SkyViewController: UIViewController {
...
func router<Event>(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<UITouch>, 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 的交互后增删改迭代功能的步骤都会变少,单独添置内容时也少有影响到别的模块,代码耦合度降低,更为清爽。那么难道就不要用 Delegates 或 Closure 了吗?我们当然不能无脑使用,既然对 ResponderChain 实现交互产生了兴趣,想必对代码美观、架构优雅是有一定要求的,不能不讲道理,那么道理怎么讲呢?笔者总结大致是下面几点:
- Subviews 视图层级只有一层、子视图回调方法较少时 用
Delegates
或Closure
- Subviews 视图层级只有一层、子视图较多(如3个以上认为已经非常繁琐) 用
ResponderChain
- Subviews 视图层级超过一层 用
ResponderChain
- 不要滥用继承 笔者深刻反思中...
冥冥之中觉得还会有更好的方式实现基于ResponderChain
的交互,还请有幸看到这里的你不吝赐教!想要把玩用例可以在这里找到。