RxSwift学习插曲--UITextField的两次输出

前言

在使用Swift的过程中,应该都使用过UITextField这个控件,这一篇就来对这个控件在RxSwift中的使用做个浅析。

问题

先来写一个UITextFieldRxSwift中的基本语法:

@IBOutlet weak var textFiled: UITextField!

textFiled.rx.text.subscribe(onNext: { (text) in
        print("你输入的是: \(text)")
    })

command+R运行代码,此时先不做任何交互操作,会发现打印出了你输入的是: Optional(""),然后点击textFiled准备输入内容,这时候再次打印了你输入的是: Optional(""),

image

之后输入内容,正常打印出的就是输入的内容,

image

也就是说在textFiled输入内容前,打印了两次空的内容,难道这是RxSwiftbug😄.

再来看另外一个问题:

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

在点击屏幕的时候,我们给textFiled赋值,看是否能订阅到赋值内容;
运行代码会发现并没有订阅到赋值的内容,但是明明已经对textFiledtext进行订阅了,text值有改变的话,居然订阅不到?

分析

首先,点击textFiled.rx.texttext进去看源码流程,

/// Reactive wrapper for `text` property.
 public var text: ControlProperty<String?> {
        return value
}

这里返回了value,相当于是valuegetter方法,再点击value跟进去:

/// Reactive wrapper for `text` property.
    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
                }
            }
        )
    }

这里返回了controlPropertyWithDefaultEvents()方法,而且这个方法传入了两个参数闭包,gettersetter,再跟进去controlPropertyWithDefaultEvents()这个方法,

 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
        )
    }

可以看到这个controlPropertyWithDefaultEvents()方法有三个参数,默认参数[.allEditingEvents, .valueChanged],以及传入的getter闭包,setter闭包;而这个方法会返回controlProperty()方法,再次跟进去看一下这个controlProperty()方法的实现;

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)
            }
            .takeUntil(deallocated)
        let bindingObserver = Binder(base, binding: setter)

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

如果在这个方法里打断点可以发现,这里是先执行·return ControlProperty(),然后才会来到Observable.create()创建序列的闭包内执行,也就是说只要执行textFiled.rx.text.subscribe(),就必然会进入到controlProperty()方法中,在这个方法里,创建序列的闭包方法内,会执行observer.on(.next(getter(control)))这句代码,这句代码就会执行一次.next,也就是说明会发送一次onNext信号,(这里就是执行的第一次打印空白的地方)

跟着代码继续往下看,会创建一个controlTarget,跟进去源码看一下controlTarget的初始化;

// 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()
        control.addTarget(self, action: selector, for: controlEvents) // 添加事件 绑定eventHandler方法
        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)
        }
    }
}

controlTargetinit方法中,传入了三个参数:control[.allEditingEvents, .valueChanged],以及传入的闭包{ _ in if let control = weakControl { observer.on(.next(getter(control))) } },这里会先将这三个参数保存下来,然后执行control.addTarget(self, action: selector, for: controlEvents),给传入的control添加事件,绑定方法,这里的control就是创建的textFiled,也就是给textFiled添加事件绑定方法,只要它开始享有就会执行selector方法,而此处绑定的selector#selector(ControlTarget.eventHandler(_:))方法,也就是说事件触发会来到ControlTarget.eventHandler(_:),在eventHandler(_:)方法内,执行callback(control)执行闭包;

{ _ in
      if let control = weakControl {
        observer.on(.next(getter(control)))
        }
    }

在闭包内执行observer.on(.next(getter(control))),这里再次.next,也就是说明会发送一次onNext信号,(这里就是执行的第二次打印空白的地方)

总结

从上面的分析中可以知道,对于问题1输出两次空白内容:在textFiled.rx.text订阅的时候,会执行一次onNext,发送信号,输出一次空白内容,在点击textFiled响应的时候会执行一次onNext,发送信号,输出一次空白内容,那么就有必要忽略掉第一次输出,解决方法是使用skip()方法;

textFiled.rx.text.skip(1).subscribe(onNext: { (text) in
        print("你输入的是: \(text)")
    })

对于问题2:textFiledtext进行赋值,subscribe闭包不执行,问题在于RxSwift的底层是event的封装,而对textFiledtext进行赋值并不是一个event事件,而且也不是一个KVO的形式,解决这个问题的方法可以在RxSwiftGitHub Issues讨论区里面看到,利用sendActions(for: .allEditingEvents)方法:

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

推荐阅读更多精彩内容