RxSwift进阶:尝试为自定义代理方法添加Reactive扩展

2,430 阅读5分钟
本文内容目录:
  • itemSelected的底层实现
  • 实战
tableView.rx.itemSelected
      .subscribe(onNext: { indexPath in
          // Todo
      })
      .disposed(by: disposeBag)

我们在使用RxSwift的时候经常会遇到这样的代码,类似的还有诸如itemDeselecteditemMoveditemInserteditemDeleted等,它们都是对UITableView代理方法进行的一层Rx封装。这样做能让我们避免因直接使用代理而不得不去做一些繁杂的工作,比如我们得去遵守不同的代理并且要实现相应的代理方法等。

而将代理方法进行Rx化,不仅会减少我们不必要的工作量,而且会使得代码的聚合度更高,更加符合函数式编程的规范。而在RxCocoa中我们也可以看到它为标准的Cocoa也同样做了大量的封装。

那么我们如何为自己的代理方法添加Reactive扩展呢?

我们先从tableView.rx.itemSelected的底层实现中探个究竟吧。

一. itemSelected的底层实现:


extension Reactive where Base: UITableView {
    // events

    /**
    Reactive wrapper for `delegate` message `tableView:didSelectRowAtIndexPath:`.
    */
    public var itemSelected: ControlEvent<IndexPath> {
        let source = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:)))
            .map { a in
                return try castOrThrow(IndexPath.self, a[1])
            }

        return ControlEvent(events: source)
    }

}
这里我们可以猜测其实现的大致流程是: 通过self.delegate触发UITableViewDelegate.tableView(_:didDeselectRowAt:)方法(即通过代理调用代理的代理方法。有点拗口,后面再理解),并通过map函数把代理方法中的IndexPath参数包装成事件流传出,供外部订阅。

再来看看其中类型:

  • self: Reactive<Base>extension Reactive where Base: UITableView,可以理解成为Base后面的UITableView添加Rx扩展。
public struct Reactive<Base> {
    /// Base object to extend.
    public let base: Base

    /// Creates extensions with base object.
    ///
    /// - parameter base: Base object.
    public init(_ base: Base) {
        self.base = base
    }
}
  • delegate: DelegateProxy 代理委托,即"代理的代理"。从这里可以回想一下上文的一句话"通过代理调用代理的代理方法",那么更为准确的说,"通过为"base"设计一个代理委托,当"base"的某个代理方法触发时,其代理委托会做出相应的响应"

我们来瞧瞧UITableView的代理委托:

/// 奇了怪了,该delegate怎么在UIScrollView的Reactive扩展里面。虽然UITableView继承自UIScrollView
但UIScrollView的代理委托怎么就能响应UITableViewDelegate的方法了? 别着急,下文给出了答案。

extension Reactive where Base: UIScrollView {
        /// Reactive wrapper for `delegate`.
        ///
        /// For more information take a look at `DelegateProxyType` protocol documentation.
        public var delegate: DelegateProxy {
            return RxScrollViewDelegateProxy.proxyForObject(base)
        }
}

那么是不是我们照葫芦画瓢地为自己的代理设计一个像这样的代理委托,就ok了呢?

暂不过早的下定论,先来瞧瞧上面提到的 DelegateProxyType,其文档解释有点长,但每个单词都很重要,且看且珍惜。

..and because views that have delegates can be hierarchical

UITableView : UIScrollView : UIView

.. and corresponding delegates are also hierarchical

UITableViewDelegate : UIScrollViewDelegate : NSObject

.. and sometimes there can be only one proxy/delegate registered,
every view has a corresponding delegate virtual factory method.

这段话解释了上文提到的delegate写在UIScrollView的Reactive扩展的疑惑。因为view和它们的delegate的响应都可以被继承下来。

DelegateProxyType.png

这是DelegateProxyType里的流程图,那么这个图说明了什么呢?

以UIScrollView为例,Delegate proxy是其代理委托,遵守DelegateProxyType与UIScrollViewDelegate,并能响应UIScrollViewDelegate的代理方法,这里我们可以为代理委托设计它所要响应的方法(即设计暴露给订阅者订阅的信号量)。(----代理转发机制)

到此,一切瞬间变得清晰起来了有木有?照葫芦画瓢设计代理委托真的就ok呀!

二. 实战

下面试着做一个简单的demo来验证一下吧!

我们首先来设计一个简(zhuo)单(lue)的使用场景:创建一个TouchView类继承自UIView并遵守TouchPointDelegate协议,下面是协议里面的一个代理方法,该代理方法的作用是返回所点击view上的point。

@objc protocol TouchPointDelegate: NSObjectProtocol {
    @objc optional func touch(at point: CGPoint, in view: UIView)
}

我们现在要为上述的代理方法添加Rx扩展,即当我们要使用该代理方法时可以像这样优雅的编码:

touchView.rx.touchPoint
            .subscribe(onNext: { point in
                print(point)
            })
            .disposed(by: disposeBag)
首先我们来设计TouchView的代理委托RxTouchViewDelegateProxy

要设计DelegateProxy(代理委托),我们先来看一眼RxScrollViewDelegateProxy是如何定义的,且要遵守哪些协议。

public class RxScrollViewDelegateProxy
: DelegateProxy,
UIScrollViewDelegate, 
DelegateProxyType {}

现在照葫芦画瓢, 定义代理委托RxTouchViewDelegateProxy并遵守相应的协议DelegateProxyDelegateProxyTypeTouchPointDelegate

class RxTouchViewDelegateProxy
: DelegateProxy, 
DelegateProxyType, 
TouchPointDelegate {}

此时会报RxTouchViewDelegateProxy没有实现DelegateProxyType协议方法的警告。

那么,我们该实现哪些协议方法呢?

不知道您有没有仔细阅读DelegateProxyType,里面有两个方法的解释中都出现了该语句Each delegate property needs to have it's own type implementing "DelegateProxyType",我们尝试实现这两个方法。

class RxTouchViewDelegateProxy: DelegateProxy, DelegateProxyType, TouchPointDelegate {
    static func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
        let touchView: TouchView = castOrFatalError(object)
        return touchView.touchDelegate
    }
    
    static func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
        let touchView: TouchView = castOrFatalError(object)
        touchView.touchDelegate = castOptionalOrFatalError(delegate)
    }
}

哎!现在终于看不到烦人的小红点了。

代理委托设计完成了,接下来就是为TouchView关联设计好的代理委托。同样的,可以先看一眼UIScrollView代理委托的关联。

extension Reactive where Base: UIScrollView {
        public var delegate: DelegateProxy {
            return RxScrollViewDelegateProxy.proxyForObject(base)
        }
}

再次照葫芦画瓢。

extension Reactive where Base: TouchView {
    var touchDelegate: DelegateProxy {
        /// RxTouchViewDelegateProxy.proxyForObject(AnyObject): 设置代理委托实例
        return RxTouchViewDelegateProxy.proxyForObject(base)
    }
}

最后,为代理委托设计它所要响应的方法(即暴露给订阅者订阅的信号量)

extension Reactive where Base: TouchView {
    // events
    
    var touchPoint: ControlEvent<CGPoint> {
        let source: Observable<CGPoint> = self.touchDelegate.methodInvoked(#selector(TouchPointDelegate.touch(at:in:)))
            .map({ a in
                return try castOrThrow(CGPoint.self, a[0])
            })
        return ControlEvent(events: source)
    }
}

因为swift编译器的bug,导致我们无法直接使用RxCocoa编写好的强转异常的处理函数,所以这里需要我们手动去拷贝这部分代码。当然啦,也可以使用可选形式的方式进行处理。如下。

    static func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
        let touchView: TouchView = (object as? TouchView)!
        return touchView.touchDelegate
    }
    
    static func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
        let touchView: TouchView = (object as? TouchView)!
        touchView.touchDelegate = delegate as? TouchPointDelegate
    }

到此我们已经成功为自定义的代理方法添加了Rx扩展。

⌘ + R BINGO!
结语

因水平有限,对Rx文档的解读难免有疏漏,请阅读本文的同时查看对应的文档内容,如有不当,请多多赐教,感谢您的阅读。

本文demo: demo

启发文: RxCocoa 源码解析--代理转发