美文网首页RxSwift学习
RxSwift中UITextField的两个问题探索

RxSwift中UITextField的两个问题探索

作者: 简_爱SimpleLove | 来源:发表于2019-08-01 23:36 被阅读0次

问题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的响应事件,当事件响应的时候,才触发方法,调用闭包,发送信号,响应订阅。

相关文章

网友评论

    本文标题:RxSwift中UITextField的两个问题探索

    本文链接:https://www.haomeiwen.com/subject/zqnfdctx.html