RxCocoa の UITextField.rx.text を購読するとリターンキーでキーボードが閉じるようになる

## はじめに

RxCocoa で UITextField の入力値をイベントストリームとして受け取る場合、`UITextField.rx.text` を使います。

```swift
import UIKit
import RxSwift
import RxCocoa

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    private let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.rx.text.subscribe().disposed(by: bag)
    }
}
```

このように `UITextField.rx.text` を `subscribe()` すると、入力値をイベントストリームとして受け取るだけでなく、なぜか、キーボードのリターンキーのタップでキーボードが閉じるようになりました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/63842/2ec597ed-fc7c-54e5-95b1-705ce8005944.gif" alt="output.gif" width="320">

RxCocoa だけでなく、ReactiveCocoa でも似たような挙動になります。

ReactiveCocoa では、次のようにして UITextField の入力値をイベントストリームとして受け取る事ができ、やはり、キーボードのリターンキーのタップでキーボードが閉じるようになります。

```swift
import UIKit
import ReactiveSwift
import ReactiveCocoa

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.reactive.continuousTextValues.observe { _ in }
    }
}
```

なぜキーボードが閉じるのか？気になったので、調べてみました。

## `UITextField.rx.text` の実装の概要

RxCocoa の [`rx.text` の実装](https://github.com/ReactiveX/RxSwift/blob/4.4.0/RxCocoa/iOS/UITextField%2BRx.swift#L16) を追っていくと、次のような実装であることが分かりました。

+ UIControl.Events の `.allEditingEvents` と `.valueChanged` をターゲットアクションに追加する
+ アクションが送信されると、UITextField の `text` プロパティを RxSwift のイベントストリームとして発行する

また、ReactiveCocoa の [`reactive.continuousTextValues` の実装](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/8.0.2/ReactiveCocoa/UIKit/UITextField.swift#L22) では `.allEditingEvents` をターゲットアクションに追加するようでした。

## リターンキーの入力でキーボードが閉じるようになる理由

RxCocoa に限らず、`.allEditingEvents` をターゲットアクションに追加すると、リターンキーの入力によってキーボードが閉じるようになります。

次のようなコードで確認することが出来ます。

```swift
import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // リターンキーの入力でキーボードが閉じるようになる
        textField.addTarget(self, action: #selector(self.hoo(sender:)), for: .allEditingEvents)
    }

    @objc func hoo(sender: Any) {
    }
}
```

`.allEditingEvents` は UITextField の全ての編集イベントを含んでいるので、`.editingDidEndOnExit` も含んでいます。

`.editingDidEndOnExit` をターゲットアクションに追加すると、リターンキーの入力でファーストレスポンダをやめるようになります。これが本質的な理由です。

次のようなコードに置き換えると `.editingDidEndOnExit` イベントが送信されないので、リターンキーをタップしてもキーボードは閉じません。

```swift
import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // リターンキーをタップしてもキーボードは閉じない
        var events = UIControl.Event.allEditingEvents
        events.subtract(.editingDidEndOnExit)
        textField.addTarget(self, action: #selector(self.hoo(sender:)), for: events)
    }

    @objc func hoo(sender: Any) {
    }
}
```

## `UITextField.rx.text` でキーボードが閉じる処理を抑止するには

キーボードが閉じるのは便利ですし、通常はこのままでも問題ないと思いますが、次のように UITextFieldDelegate に適合することで、この挙動を抑止することも出来ます。

+ `textFieldShouldReturn(_:)` で `false` を返す
+ `textFieldShouldEndEditing(_:)` で `false` を返す

`textFieldShouldReturn(_:)` で `false` を返した場合は、`.editingDidEndOnExit` が送信されなくなるので、**リターンキーをタップしても `UITextField.rx.text` の next イベントが発行されず、キーボードを閉じることを抑止することができます。**

```swift
final class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet private weak var textField: UITextField!

    private let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.delegate = self

        textField.rx.text
            .bind(onNext: { print($0.debugDescription) })
            .disposed(by: bag)
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // onNext は呼ばれない、キーボードも閉じない
        return false
    }
}
```

`textFieldShouldEndEditing(_:)` は `.editingDidEndOnExit` の送信後に呼ばれるので、**リターンキーのタップで `UITextField.rx.text` の next イベントが発行され、かつキーボードが閉じるを抑止することができます。**

```swift
final class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet private weak var textField: UITextField!

    private let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.delegate = self

        textField.rx.text
            .bind(onNext: { print($0.debugDescription) })
            .disposed(by: bag)
    }

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        // onNext は呼ばれるが、キーボードは閉じない
        return false
    }
}
```

ということで、意図に合わせて UITextFieldDelegate を実装すれば良さそうです。
RxCocoa ではなく、UIKit の仕様に依存した内容なので、ReactiveCocoa も同様の制御が可能です。

## まとめ

+ `rx.text` を購読するとリターンキーのタップでキーボードが閉じるようになる
+ この挙動は `.editingDidEndOnExit` をターゲットアクションに追加した際の UIKit の標準的な挙動である
+ この挙動を抑止したい場合は UITextFieldDelegate を実装する