06. RxSwift源码解读:ControlEvent、ControlProperty、Binder

今天带大家解读RxSwift中封装UI事件响应相关的源码:

ControlEvent和ControlProperty

ControlEvent 专门用于描述 UI 控件所产生的事件,ControlProperty专门描述 UI 控件属性,它们具有以下特征:

  • 不会产生 error 事件
  • 一定在 MainScheduler 订阅(主线程订阅)
  • 一定在 MainScheduler 监听(主线程监听)。
    这两个都是可观察序列。
    两者的区别在于ControlProperty可以作为观察者接受消息,比如他可以作为一个Binder;而ControlEvent不可以。看下面的例子:
        btn.rx.tap.subscribe {
            print($0)
        }
        .disposed(by: bag)
        textFiled.rx.text.subscribe {
            print($0)
        }
        .disposed(by: bag)

一个Button调用rx.tap转换成一个序列, 在源码中实际上就是包装成一个ControlEvent,这样将Button的点击事件转换成序列,并可通过subscribe订阅这个事件,所以每次点击按钮会打印next事件。

    public var tap: ControlEvent<Void> {
        controlEvent(.touchUpInside)
    }
public func controlEvent(_ controlEvents: UIControl.Event) -> ControlEvent<()> {
        let source: Observable<Void> = Observable.create { [weak control = self.base] observer in
                MainScheduler.ensureRunningOnMainThread()

                guard let control = control else {
                    observer.on(.completed)
                    return Disposables.create()
                }

                let controlTarget = ControlTarget(control: control, controlEvents: controlEvents) { _ in
                    observer.on(.next(()))
                }

                return Disposables.create(with: controlTarget.dispose)
            }
            .take(until: deallocated)

        return ControlEvent(events: source)
    }

上面的代码将点击事件封装成可观察序列,内部通过Observable.create创建了一个可观察序列,并且将此序列所谓source保存在ControlEvent的对象中。在subscribe handler中,包装一个ControlTarget对象,ControlTarget初始化方法中对UIControl添加点击事件
control.addTarget(self, action: selector, for: controlEvents)
然后处理点击事件。

   @objc func eventHandler(_ sender: Control!) {
        if let callback = self.callback, let control = self.control {
            callback(control)
        }
    }

在callback的实现中,调用了onNext, 回到调用的地方:

let controlTarget = ControlTarget(control: control, controlEvents:     controlEvents) { _ in
                    observer.on(.next(()))
                }

这样就能产生ui点击事件,它将点击事件变换为onNext(())。

ControlEvent是一个结构体,遵循了ControlEventType协议,ControlEventType协议又继承了ObservableType,所以它是一个可观察序列。

除了上面的UIControl,任何对象都可以可序列化,只要遵循了ReactiveCompatible的协议,ReactiveCompatible实现了rx属性的get set方法,通过这种方式创建一个Reactive对象,同时Reactive对象有一个属性base,这个base就是被序列化的对象。
比如 上面的代码 btn.rx 等价于 Reactive(btn), 然后再通过扩展Reactive,创建一个可观察序列即可。

回到例子中的代码继续看textFiled.rx,同样是对UITextFiled转换成Reactive<UITextFiled>类型对象。调用text时调用了controlPropertyWithDefaultEvents

    public var value: ControlProperty<String?> {
        return base.rx.controlPropertyWithDefaultEvents(
            getter: { textField in
                textField.text
            },
            setter: { textField, value in
                // This check is important because setting text value always clears control state
                // including marked text selection which is imporant for proper input 
                // when IME input method is used.
                if textField.text != value {
                    textField.text = value
                }
            }
        )
    }

传入getter和setter闭包
getter是取出textfiled的text,setter是更新textfiled的text。

    public func controlProperty<T>(
        editingEvents: UIControl.Event,
        getter: @escaping (Base) -> T,
        setter: @escaping (Base, T) -> Void
    ) -> ControlProperty<T> {
        let source: Observable<T> = Observable.create { [weak weakControl = base] observer in
                guard let control = weakControl else {
                    observer.on(.completed)
                    return Disposables.create()
                }

                observer.on(.next(getter(control)))

                let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
                    if let control = weakControl {
                        observer.on(.next(getter(control)))
                    }
                }
                
                return Disposables.create(with: controlTarget.dispose)
            }
            .take(until: deallocated)

        let bindingObserver = Binder(base, binding: setter)

        return ControlProperty<T>(values: source, valueSink: bindingObserver)
    }

这里封装了一个ControlProperty,绑定一个Binder。如果当前ControlProperty作为一个Observer,这个Binder才有用。

ControlTarget封装了UITextField的text改变的事件,通过callback通知调用者,然后通过observer.on(.next(getter(control)))将当前UITextFiled的text值发出去。

我们进入ControlProperty结构体看看

public struct ControlProperty<PropertyType> : ControlPropertyType {
    public typealias Element = PropertyType

    let values: Observable<PropertyType>
    let valueSink: AnyObserver<PropertyType>
/...

ControlProperty遵循了ControlPropertyType协议,而ControlPropertyType继承了ObservableType和ObserverType。所以既可以作为可观察序列又可以作为观察者,比如可以作为一个Binder。

作为可订阅序列,如例子中当我们订阅它时,一旦text改变,就可以接受到事件,订阅时执行的是values的订阅方法。订阅逻辑和第一章讲到的订阅逻辑一样,这里不再重述。

ControlProperty包含一个 values和一个valueSink,values是原始的observable,而且保证了在主线程执行subscribe handler。

values.subscribe(on: ConcurrentMainScheduler.instance)

valueSink是一个Binder,ControlProperty实现了on方法:

/// Binds event to user interface.
    ///
    /// - In case next element is received, it is being set to control value.
    /// - In case error is received, DEBUG buids raise fatal error, RELEASE builds log event to standard output.
    /// - In case sequence completes, nothing happens.
    public func on(_ event: Event<Element>) {
        switch event {
        case .error(let error):
            bindingError(error)
        case .next:
            self.valueSink.on(event)
        case .completed:
            self.valueSink.on(event)
        }
    }

如果将ControlProperty作为一个Observer来使用,这里的on方法会接受到事件,同时将next和completed事件转发给valueSink进行处理,valueSink是在初始化ControlProperty赋值的:

        let bindingObserver = Binder(base, binding: setter)
        return ControlProperty<T>(values: source, valueSink: bindingObserver)

将setter闭包赋值给binding,setter就是最开始创建序列时定义的,用来设置UITextFiled的text

setter: { textField, value in
                // This check is important because setting text value always clears control state
                // including marked text selection which is imporant for proper input 
                // when IME input method is used.
                if textField.text != value {
                    textField.text = value
                }
            }

所以我们进入Binder类看一看:

/// Initializes `Binder`
    ///
    /// - parameter target: Target object.
    /// - parameter scheduler: Scheduler used to bind the events.
    /// - parameter binding: Binding logic.
    public init<Target: AnyObject>(_ target: Target, scheduler: ImmediateSchedulerType = MainScheduler(), binding: @escaping (Target, Value) -> Void) {
        weak var weakTarget = target

        self.binding = { event in
            switch event {
            case .next(let element):
                _ = scheduler.schedule(element) { element in
                    if let target = weakTarget {
                        binding(target, element)
                    }
                    return Disposables.create()
                }
            case .error(let error):
                rxFatalErrorInDebug("Binding error: \(error)")
            case .completed:
                break
            }
        }
    }

    /// Binds next element to owner view as described in `binding`.
    public func on(_ event: Event<Value>) {
        self.binding(event)
    }

当调用valueSink的on方法时,会调用binding方法,也就是执行setter方法,完成UITextFiled 的text更新,而且为了保证更新操作再主线程执行,上面代码中默认的调度器是MainScheduler。
下面是一个例子,演示一个UITextField绑定另一个UITextField:

        // 两个rx.text 返回的都是ControlProperty对象
        let binder = textFiled2.rx.text
        textFiled.rx.text.bind(to: binder)
        .disposed(by: bag)

UITextFiled 也可以绑定attributedText;代码实现与绑定text类似。

Binder any thing

除了可以绑定UITextFiled,我们可以封装任意属性为一个Binder,比如lable.rx.text, 只需要在对象后面调用.rx.属性名, 这样就转换成一个Binder. 如:

textFiled.rx.text.bind(to: label.rx.text)
        .disposed(by: bag)

这样text一旦发生改变,就会降其值赋值给label.text, 这就是绑定。我们看看label.rx.text方法实现

/// Automatically synthesized binder for a key path between the reactive
    /// base and one of its properties
    public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Property>) -> Binder<Property> where Base: AnyObject {
        Binder(self.base) { base, value in
            base[keyPath: keyPath] = value
        }
    }

这里利用swift的keypath实现了任意属性的绑定,代码非常简洁巧妙。
然后再看看bind方法实现:

public func bind<Observer: ObserverType>(to observers: Observer...) -> Disposable where Observer.Element == Element {
        self.subscribe { event in
            observers.forEach { $0.on(event) }
        }
    }

实际上就是调用订阅方法,而且可以绑定多个Observer。

总结

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

推荐阅读更多精彩内容