RxSwift中UITextField的两个问题探索

问题1

        _ = textFiled.rx.text.subscribe(onNext: { (text) in
            print("输入来了 \(text)")
        })
        _ = textView.rx.text.subscribe(onNext: { (text) in
            print("textView:输入来了 \(text)")
        })

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        textFiled.text = "Cooci"
        textView.text = "LGCooci"
    }

当点击屏幕的时候,textFiled的text变为了Cooci,但是并没有走到textFiled的订阅方法subscribe中去。但是textView.text变为了LGCooci,且走到了textView的订阅方法subscribe中去了。

分析

textView

点击textView的text进去看文档部分如下:

 public var value: ControlProperty<String?> {
        let source: Observable<String?> = Observable.deferred { [weak textView = self.base] in
            let text = textView?.text
            let textChanged = textView?.textStorage
            // This project uses text storage notifications because
            // that's the only way to catch autocorrect changes
            // in all cases. Other suggestions are welcome.

由官方注释可知:这个项目使用了通知来进行传递text的变化,因为这是唯一一种方式可以捕捉在任何情况下值的变化。欢迎提出其他建议。

textFiled

再来看textFiled的text文档,会发现一段关键代码:

    /// This is a separate method to better communicate to public consumers that
    /// an `editingEvent` needs to fire for control property to be updated.
    internal func controlPropertyWithDefaultEvents<T>(
        editingEvents: UIControl.Event = [.allEditingEvents, .valueChanged],
        getter: @escaping (Base) -> T,
        setter: @escaping (Base, T) -> Void
        ) -> ControlProperty<T> {
        return controlProperty(
            editingEvents: editingEvents,
            getter: getter,
            setter: setter
        )
    }

官方注释:一个编辑事件状态即editingEvent需要更新controltextFiled的属性,也即是要更新controlProperty

所以textFiled是根据编辑事件来响应订阅的,这里有allEditingEventsvalueChanged两种。其中valueChanged看文档是对UISwitch的响应,allEditingEvents是对textFiled的响应,因为textFiled.text = "Cooci"只是一个赋值,textFiled并没有进入编辑状态,所以没有走到textFiled的订阅方法subscribe中去。

解决办法

在赋值的后面跟上sendActions方法,就可以走到textFiled的订阅方法subscribe中去。如下:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        textFiled.text = "Cooci"
        textFiled.sendActions(for: .allEditingEvents)
        textView.text = "LGCooci"
    }

问题2

        _ = textFiled.rx.text
             .subscribe(onNext: { (text) in
            print("输入来了 \(text)")
        })
        /*
         输入来了 Optional("")   //初始化时的打印
         输入来了 Optional("")   //点击了textFiled后的打印
         */

上面这样写过后,输入会走两次,一次是初始化的时候,一次是点击textFiled进入编辑状态时。

分析

所有的分析,我们就需要打断点,然后看源码来进行分析。

我们来到一段关键代码:

    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 { // 当前后的textField不是同一个时,就发送结束信号
                    observer.on(.completed)
                    return Disposables.create()
                }

                observer.on(.next(getter(control))) // skip(1) 是防止初始化的这里调起一次发送信号

            // 类ControlTarget封装了textField的事件响应方法
            // 初始化的时候,textField没有响应,所以没有走下面ControlTarget事件响应方法,也就不会发送信号
            // 这个大括号是一个尾随闭包
                let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
                    if let control = weakControl {  // 当前后的textField是同一个时,才发送信号
                        observer.on(.next(getter(control)))
                    }
                }
                
                return Disposables.create(with: controlTarget.dispose)
            }
            .takeUntil(deallocated)

        let bindingObserver = Binder(base, binding: setter)

        return ControlProperty<T>(values: source, valueSink: bindingObserver)   // 是一个结构体
    }

上面代码的主要关键点:

  • ControlProperty是一个里面实现了subscribe方法的结构体,传进去了序列和观察者两个参数,并在结构体内部实现两者之间的调度关系。
  • ControlTarget作为一个中间类,封装了textField的响应方法,并传了一个尾随闭包进去。
  • 当只有ControlProperty被订阅初始化的时候,才会走到observer.on(.next(getter(control)))这句发送信号的代码,也即是第一次打印。
  • 但是当ControlProperty被订阅初始化的时候,textField并没有被响应,所以不会走ControlProperty闭包中的发送信号。

然后的关键代码就是类ControlProperty里面的代码:

// This should be only used from `MainScheduler`
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: UIControl.Event
#endif
    var callback: Callback?
    #if os(iOS) || os(tvOS)
    init(control: Control, controlEvents: UIControl.Event, callback: @escaping Callback) {
        MainScheduler.ensureRunningOnMainThread()
        // 保存了传进来的参数
        self.control = control
        self.controlEvents = controlEvents
        self.callback = callback

        super.init()
        // 添加响应事件方法,当controlEvents即UIControl.Event发送变化是,就响应selector方法,即eventHandler闭包
        control.addTarget(self, action: selector, for: controlEvents)

        let method = self.method(for: selector)
        if method == nil {  // 如果没有找到方法就返回一个错误
            rxFatalError("Can't find method")
        }
    }
    @objc func eventHandler(_ sender: Control!) {
        if let callback = self.callback, let control = self.control {
            callback(control) // 当event发生改变的时候,一直响应这个闭包
        }
    }

上面代码关键点:

  • 初始化的时候,保存了传进来的三个参数,并添加了响应事件方法。
  • 事件方法中调用了外面传进来的闭包callback(control)。也就是说,每次event事件改变的时候,都走事件响应方法,即callback(control),也就是上一段代码中传进来的那个尾随闭包:
                let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
                    if let control = weakControl {  // 当前后的textField是同一个时,才发送信号
                        observer.on(.next(getter(control)))
                    }
                }

大括号中的代码,发送信号,从而响应订阅。

解决办法

第一种:
可以注释掉源代码中的observer.on(.next(getter(control)))这句代码,从而订阅初始化的时候,就不会有第一次打印了。

但是我们一般最好不要改源代码,所以就第二种方法。

第二种:
使用方法skip,跳过那一次的发送信号,如下:

        _ = inputTF.rx.text.skip(1)
            .subscribe(onNext: { (text) in
            print("输入:\(text)")
        })

这样也就没有了订阅初始化时候的那第一次响应。

关键思想

使用中间类封装了textField的响应事件,当事件响应的时候,才触发方法,调用闭包,发送信号,响应订阅。

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

推荐阅读更多精彩内容