RxSwift源码探秘 :如何让UIControl生成可观察序列

我们在使用RxSwift进行响应式编程时,都会引入另一个库RxCocoa,RxCocoa是Rx对iOS的原生API中UIKit以及Foundation中的视图(UIView)、控制事件(Control Event)、键值观察(KVO)、通知(Notification)等的扩展,以便在开发时更方便的对系统的这些原生组件进行Rx应用。例如:

//registerButton是一个UIButton,通过.rx.tap能获取到该button的点击事件序列
_ = registerButton.rx.tap.shareReplay(1).subscribe {(_) in
     //订阅者获取到按钮点击的事件,再做处理
 }.disposed(by: disposeBag)

//nameTextField是一个UITextField控件,可以直接通过.rx.text获取到该控件中输入内容的String序列
_ = nameTextField.rx.text.shareReplay(1).subscribe {(text) in
     //订阅者实时获取到text内容,再做处理
 }.disposed(by: disposeBag)

上述例子中通过registerButton.rx.tap,我们可以获取该button的点击事件序列,而nameTextField.rx.text给我们提供了UITextField中的文本String的序列,这就像我们通过正常的编码方式获取到的一样:

//给registerButton添加Target和点击事件
registerButton.addTarget(self, action: #selector(registerButtonClick), for: .touchUpInside)

@objc func registerButtonClick(){
    //注册按钮点击了
 }

//nameTextField通过通知观察获得UITextField变化的文本
NotificationCenter.default.addObserver(self, selector: #selector(registerButtonClick), name: NSNotification.Name.UITextFieldTextDidChange, object: nil)

@objc func nameTextFieldValueChanged(){
    //获取到UITextField的text
 }

对比两者不难发现,系统原生的方法有种割裂感,而RxCocoa提供的被观察者以及订阅的模式更符合我们正常思考的模式,在事件发生变化后及时作出回应,而这正是RxSwift响应式编程的核心思想。要达到这种响应式编程的前提条件就是先得有一个可被观察的序列Observable,而RxCocoa中又是怎么将UIButton和UITextField这些继承于UIControl的控件的控制事件转化为一个个不同的Observable事件序列的呢,本篇文章就以UIButton和UITextField为例,探寻RxCocoa源码中的奥秘。

Observable的创建

RxSwift中,在Observable的扩展类里提供了一系列创建被观察对象的工厂化方法。例如通过never、empty、just、of等来创建不同的Observable序列,例如:

Observable.just("🔴")
        .subscribe { event in
            print(event)
        }
        .disposed(by: disposeBag)

Observable.of("🐶", "🐱", "🐭", "🐹")
        .subscribe(onNext: { element in
            print(element)
        })
        .disposed(by: disposeBag)

但如何将一个UIControl的Control Event(包括将来即将发生的点击事件、text变化等等)也转换成对应的序列呢,以上方法好像并不适用,这时就需要用到自定义创建Observable方法了,来看看官方示例:

let myJust = { (element: String) -> Observable<String> in
        return Observable.create { observer in
            observer.on(.next(element))
            observer.on(.completed)
            return Disposables.create()
        }
    }
        
myJust("🔴").subscribe { 
        print($0) 
     }.disposed(by: disposeBag)

通过Observable.create方法可以得到一个Observable<E>序列,E是一个泛型类型,在上述例子中E就是String类型

public static func create(_ subscribe: @escaping (AnyObserver<E>) -> Disposable) -> Observable<E> {
        return AnonymousObservable(subscribe)
    }

而要想达到持续发送事件的目的,只要在@escaping (AnyObserver<E>) -> Disposable) 这个闭包中进行observer.on(.next(element))操作就可以将由element构成的序列发送给订阅者了。
注意:此处的观察者AnyObserver<E>的观察类型E与你生成的被观察者类型Observable<E>的E要确保一致。而RxCocoa中正是在UIControl扩展中使用了自定义创建Observable.create方法来处理并转化对应的事件。

UIControl+Rx

下面分别以UIButton和UITextField为例,来探寻UIControl+Rx这个Reactive限定扩展中的秘密。(关于限定扩展可以看我上一篇文章)
先来看通过UIButton.rx.tap我们得到了一个什么东西:

extension Reactive where Base: UIButton {
    
    /// Reactive wrapper for `TouchUpInside` control event.
    public var tap: ControlEvent<Void> {
        return controlEvent(.touchUpInside)
    }
}

我们得到一个ControlEvent<Void>,而ControlEvent<Void>是什么呢

//ControlEvent<Void>遵循ControlEventType协议
public struct ControlEvent<PropertyType> : ControlEventType
//ControlEventType遵循ObservableType协议
public protocol ControlEventType : ObservableType 

而我们知道,遵循ObservableType就可以当做一个Observable序列,所以通过对UIButton的.rx.tap我们得到了一个不含任何数据(即Void)的事件序列,再来进一步对源码分析,此处返回的ControlEvent<Void>是通过controlEvent(.touchUpInside)得到的,来看controlEvent方法:

extension Reactive where Base: UIControl {
    
    /// - parameter controlEvents: Filter for observed event types.
    ///此处传入UIControlEvents控制事件类型,以上UIButton传入的是.touchUpInside点击事件
    public func controlEvent(_ controlEvents: UIControlEvents) -> ControlEvent<Void> {

        //通过Observable.create自定义创建一个Observable
        let source: Observable<Void> = Observable.create { [weak control = self.base] observer in
            MainScheduler.ensureExecutingOnScheduler()

            guard let control = control else {
                observer.on(.completed)  // 如果self.base为空,则发送完成事件
                return Disposables.create()
            }
            //通过控件control和事件controlEvents初始化一个ControlTarget类,
            //在control触发controlEvents事件时,通过闭包回调回来(详见后面)
            let controlTarget = ControlTarget(control: control, controlEvents: controlEvents) { control in
                observer.on(.next())   //一旦有回调,就给订阅者发送事件
            }
        
            return Disposables.create(with: controlTarget.dispose)
        }.takeUntil(deallocated)
        //将自定义序列source包装成ControlEvent返回
        return ControlEvent(events: source)
    }

ControlTarget

以上源码中最关键是ControlTarget这个初始化方法,通过控件control(UIButton、UITextField等等)和事件controlEvents初始化一个ControlTarget类,在control触发controlEvents事件时,通过闭包回调回来

final class ControlTarget: RxTarget {
    typealias Callback = (Control) -> Void
    let selector: Selector = #selector(ControlTarget.eventHandler(_:))
    weak var control: Control?
#if os(iOS) || os(tvOS)
    let controlEvents: UIControlEvents
#endif
    var callback: Callback?
    #if os(iOS) || os(tvOS)
   //传入控件control(UIButton、UITextField等等)和事件controlEvents
    init(control: Control, controlEvents: UIControlEvents, callback: @escaping Callback) {
        MainScheduler.ensureExecutingOnScheduler()
        self.control = control
        self.controlEvents = controlEvents
        self.callback = callback   //等待self.callback回调
        super.init()

        //将controlEvents事件添加到自己,响应selector方法
        control.addTarget(self, action: selector, for: controlEvents)
        let method = self.method(for: selector)
        if method == nil {
            rxFatalError("Can't find method")
        }
    }

该事件触发的方法为#selector(ControlTarget.eventHandler(_:)),eventHandler方法是怎么处理的呢,

let selector: Selector = #selector(ControlTarget.eventHandler(_:))

//控制事件controlEvents所需要触发的方法
func eventHandler(_ sender: Control!) {
      if let callback = self.callback, let control = self.control {
          callback(control) //在self.control和callback都不为空时 回调
      }
 }

一目了然,通过上面的callback(control) 我们就能在下面这个回调函数中给订阅者发送事件了

let controlTarget = ControlTarget(control: control, controlEvents:controlEvents) { control in
     observer.on(.next())   //一旦有回调,就给订阅者发送事件
}

同理,我们再来看看UITextField.rx.text生成String可观察序列的代码:

extension Reactive where Base: UITextField {
    /// Reactive wrapper for `text` property.
    public var text: ControlProperty<String?> {
        return value
    }
    
    /// Reactive wrapper for `text` property.
    public var value: ControlProperty<String?> {
         //UIControl.rx.value方法是关键
        return UIControl.rx.value(
            base,
            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
                }
            }
        )
    } 
}

接着看UIControl.rx.value这个方法的源码:

//参数为控件本身,其他分别为两个闭包,
//其中getter是用来获取UITextField的text值发送给订阅者,setter是设置数值
static func value<C: UIControl, T>(_ control: C, getter: @escaping (C) -> T, setter: @escaping (C, T) -> Void) -> ControlProperty<T> {
        let source: Observable<T> = Observable.create { [weak weakControl = control] observer in
                guard let control = weakControl else {
                    observer.on(.completed)
                    return Disposables.create()
                }

                observer.on(.next(getter(control))) //订阅后首次发送事件
                //通过ControlTarget来监听UITextField所有跟编辑有关以及值修改的控制事件
                let controlTarget = ControlTarget(control: control, controlEvents: [.allEditingEvents, .valueChanged]) { _ in
                    if let control = weakControl {
                        observer.on(.next(getter(control))) //一旦有变化就发送给订阅者
                    }
                }
                
                return Disposables.create(with: controlTarget.dispose)
            }
            .takeUntil((control as NSObject).rx.deallocated)
        let bindingObserver = UIBindingObserver(UIElement: control, binding: setter)
        return ControlProperty<T>(values: source, valueSink: bindingObserver)
    }

通过ControlTarget来监听UITextField所有跟编辑有关以及value值发生改变的控制事件,最后生成一个ControlProperty类型的Observable可观察序列,然后发送给订阅者。

以上就是RxCocoa中如何通过自定义创建Observable,将UIControl控件的控制事件转化为Observable可观察序列的。

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

推荐阅读更多精彩内容

  • 最近在学习RxSwift相关的内容,在这里记录一些基本的知识点,以便今后查阅。 Observable 在RxSwi...
    L_Zephyr阅读 1,700评论 1 4
  • 本章将向你介绍另一个框架,它是原生RxSwift库的一部分:RxCocoa。 RxCocoa全平台通用。每个平台有...
    大灰很阅读 824评论 3 2
  • 作者: maplejaw本篇只解析标准包中的操作符。对于扩展包,由于使用率较低,如有需求,请读者自行查阅文档。 创...
    maplejaw_阅读 45,336评论 8 93
  • 岁月慢慢磨平了棱角,已经过了飞蛾的年纪,那就好好的做只候鸟。只愿此生淡然!越简单越纯粹。
    困了可以想睡就睡么阅读 223评论 0 0
  • 这个framework级别的动画效果有以下特点: Group-level animations在一个View Hi...
    lxacoder阅读 708评论 0 1