iOS
Swift
RxSwift

UITextField.rx.textがイベントを発行するタイミング

UITextField.rx.textがイベントを発行するタイミングについてまとめました。

イベント一覧

操作内容 UITextField.rx.textイベントの発生回数 同時に発行されるUIControl.Eventイベント
サブスクライブ 1回 なし
テキストフィールドをタップしてフォーカスを当てる 1回 editingDidBegin
画面タップによってフォーカスを外す 1回 editingDidEnd
キーボードで1文字入力する 1回 editingChanged
キーボードでリターンキーをタップする 2回 editingDidEndOnExitとeditingDidEnd
UITextField.textにコードから文字を代入する 0回 なし
UITextField.textにコードから文字を代入し、valueChangedメッセージを送る 1回 valueChanged

キーボードでリターンキーをタップした時、入力されている文字列が2回ストリームに流れてくるので、UITextField.rx.textをサブスクライブしてAPIリクエストを行うなどの処理をしている場合は余計な通信が発生しないよう注意が必要です。
下記で紹介しているコードを見ていただければと思いますが、こうした余計なイベントへの対応としてUITextField.isEditingプロパティが使えるかもしれません。

また、UITextField.textにコードから文字列を代入した場合、UITextField.sendActions(for: .valueChanged)を実行しないとUITextField.rx.textイベントが流れてこない点も気をつけましょう。

画面とコード

テストに使用した画面とコードを以下に載せておきます。

画面は一つのUITextFieldと二つのUIButtonで構成されています。

image.png

ViewControllerの実装は以下の通りです。
UITextField.rx.textプロパティおよびUITextFieldに関連するイベントをサブスクライブしています。
また、ViewControllerのviewプロパティにタップジェスチャーを追加しています。

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var button2: UIButton!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let gesture = UITapGestureRecognizer()
        view.addGestureRecognizer(gesture)

        gesture.rx.event.asDriver()
            .drive(onNext: { [unowned self] event in
                print("tap event: \(event)")
                self.view.endEditing(true)
            })
            .disposed(by: disposeBag)

        textField.rx.text.orEmpty.asDriver()
            .drive(onNext: { [unowned self] text in
                print("text: \(text)")
                print("isEditing: \(self.textField.isEditing)")
            })
            .disposed(by: disposeBag)

        textField.rx.controlEvent(.editingDidBegin).asDriver()
            .drive(onNext: { _ in
                print("editingDidBegin")
            })
            .disposed(by: disposeBag)

        textField.rx.controlEvent(.editingChanged).asDriver()
            .drive(onNext: { _ in
                print("editingChanged")
            })
            .disposed(by: disposeBag)

        textField.rx.controlEvent(.editingDidEnd).asDriver()
            .drive(onNext: { _ in
                print("editingDidEnd")
            })
            .disposed(by: disposeBag)

        textField.rx.controlEvent(.editingDidEndOnExit).asDriver()
            .drive(onNext: { _ in
                print("editingDidEndOnExit")
            })
            .disposed(by: disposeBag)

        textField.rx.controlEvent(.valueChanged).asDriver()
            .drive(onNext: { _ in
                print("valueChanged")
            })
            .disposed(by: disposeBag)

        button.rx.tap.asDriver()
            .drive(onNext: { [unowned self] _ in
                print("button tapped")
                self.textField.text = "button"
            })
            .disposed(by: disposeBag)

        button2.rx.tap.asDriver()
            .drive(onNext: { [unowned self] _ in
                print("button2 tapped")
                self.textField.text = "button2"
                self.textField.sendActions(for: .valueChanged)
            })
            .disposed(by: disposeBag)
    }
}

実行ログ

画面操作内容とその時の出力ログです。

画面表示直後

text: 
isEditing: false

テキストフィールドをタップしてフォーカスを当てた時

text:
isEditing: true
editingDidBegin

画面をタップしてテキストフィールドからフォーカスを外した時

tap event: <UITapGestureRecognizer: 0x6040001f3d00; state = Ended; view = <UIView 0x7f8f4a50a130>; target= <(action=eventHandler:, target=<_TtGC7RxCocoa13GestureTargetCSo22UITapGestureRecognizer_ 0x600000252690>)>>
text:
isEditing: false
editingDidEnd

"a"という文字を入力した時

text: a
isEditing: true
editingChanged

"a"という文字が入力された状態でリターンキーをタップした時

text: a
isEditing: true
editingDidEndOnExit
text: a
isEditing: false
editingDidEnd

"Button"をタップした時

button tapped

"Button2"をタップした時

button2 tapped
text: button2
isEditing: false
valueChanged