Swift 复杂视图多事件回调处理方案思考

开发中难免有需要制作复杂 UI 的 ViewController,子视图一层套一层,夸张时层层视图都有事件回调,而我们只想在 “C” 中接收回调并处理,能让代码明明白白在业务处理层中展现,让场景易于维护、低耦合、迭代时清晰高效是我们所要达成的最终目的。

这篇文章主要是对结合《一种基于ResponderChain的对象交互方式》的方式与《关于使用 Swift 协议确保类型的思考》的思考与实践。文中涉及到有关 Swift 消息派发机制知识,推荐文章《深入理解 Swift 派发机制》

贯穿全文的例子

假设一个业务场景作为示例:做一个关于天空复杂视图(View),天空中有一个太阳(View),太阳里有太阳黑子(View),这里点击天空或太阳、太阳的Subview都要直接在(ViewController)中做出相应处理。

Swift 复杂视图多事件回调处理方案思考_第1张图片
SkyResponderChainPlayground.swift.png

为了让代码简洁更好说明,点击事件使用简单的 touchesBegan(_:with:) 方法。

代理或闭包回调

闭包相比代理能少很多 XXXViewDelegate 及其实现,但是从架构上看,closure 的处理代码清晰程度必然比不上 delegate,因为创建时机等关系,closure 的代码可能散落各地,而 delegate 回调可以放在一目了然的位置,更加易于debug。

Swift 复杂视图多事件回调处理方案思考_第2张图片
Delegates Closure.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

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 后添加一个传递方法。其原理不是文章主要内容,不过多赘述,资料非常多。

Swift 复杂视图多事件回调处理方案思考_第3张图片
Responder Chain.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 中类的拓展函数是被静态派发的,所以无法被子类继承,我们也不能将代码直接添加到 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(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) 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 ResponderChainTyperouter(:)的注释// 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 增删改所需步骤的繁琐程度。

Swift 复杂视图多事件回调处理方案思考_第4张图片
SkyViewController.png
增删:新增太阳耀斑视图 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 视图层级只有一层、子视图回调方法较少时 用DelegatesClosure
  • Subviews 视图层级只有一层、子视图较多(如3个以上认为已经非常繁琐) 用ResponderChain
  • Subviews 视图层级超过一层 用ResponderChain
  • 不要滥用继承

你可能感兴趣的:(Swift 复杂视图多事件回调处理方案思考)