ios收藏swift

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

2018-08-18  本文已影响31人  wiiale

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

这片文章主要是对结合《一种基于ResponderChain的对象交互方式》的方式与《关于使用 Swift 协议确保类型的思考》的思考与实践。

贯穿全文的例子

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

SkyResponderChainPlayground.swift.png

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

代理或闭包回调

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

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

把 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")
    }
}

这是笔者以前使用的一套模式,这样继承存在非常大的问题

在功能迭代时,很难满足上述两点,即便 updateView 默认实现处不给激进的 fatalError 错误处理,也不能保证之后版本的迭代有的 View 不是动态的更新方法,或是有的视图根本不存在回调,导致代码越来越难看,所以继承要慎用(深刻反思)。并且这么做也没有解决层层回调代码过与复杂的问题。

响应链

如何汲取继承的好处,将事件放在一块儿维护?

ResponderChain 就是一个可以实现这样需求的机制。虽说 UIResponder 也是逐级传递,但对开发者来说不必逐级实现传递功能。只需要 extension UIResponder 后添加一个传递方法。其原理不是文章主要内容,不过多赘述,资料非常多。

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 并不支持在拓展中重写父类方法,只能添加新方法。这里需要曲线处理。解决办法是在 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 优化

主要存在的问题:

使用泛型协议

喵神在文章开头提到里提到:「相比于 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 ResponderChainTyperouter<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 增删改所需步骤的繁琐程度。

SkyViewController.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 实现交互产生了兴趣,想必对代码美观、架构优雅是有一定要求的,不能不讲道理,那么道理怎么讲呢?笔者总结大致是下面几点:

冥冥之中觉得还会有更好的方式实现基于ResponderChain的交互,还请有幸看到这里的你不吝赐教!想要把玩用例可以在这里找到。

上一篇下一篇

猜你喜欢

热点阅读