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

本文内容架构:
  • 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 源码解析--代理转发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • RxSwift_v1.0笔记——13 Intermediate RxCocoa 这章将学习一些高级的RxCocoa...
    大灰很阅读 601评论 1 1
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,036评论 29 470
  • 我与你的生活 像风一样自由 阳光透过青苔的瓦檐 柔和细腻 我扬起脸庞 透过炊烟 看见湛蓝划破天际 天上的云朵寻觅 ...
    清风浦上阅读 125评论 0 1
  • 昨天上午三次,下午一次,吐过之后的虚弱不堪,艾玛,我真怕了。 今天正儿八经的只吐了晚上这一回狠的,早上中午下午的都...
    我是蔷薇阅读 111评论 0 0