はじめに
RxCocoa で UITextField の入力値をイベントストリームとして受け取る場合、UITextField.rx.text を使います。
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() すると、入力値をイベントストリームとして受け取るだけでなく、なぜか、キーボードのリターンキーのタップでキーボードが閉じるようになりました。
RxCocoa だけでなく、ReactiveCocoa でも似たような挙動になります。
ReactiveCocoa では、次のようにして UITextField の入力値をイベントストリームとして受け取る事ができ、やはり、キーボードのリターンキーのタップでキーボードが閉じるようになります。
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 の実装 を追っていくと、次のような実装であることが分かりました。
- UIControl.Events の
.allEditingEventsと.valueChangedをターゲットアクションに追加する - アクションが送信されると、UITextField の
textプロパティを RxSwift のイベントストリームとして発行する
また、ReactiveCocoa の reactive.continuousTextValues の実装 では .allEditingEvents をターゲットアクションに追加するようでした。
リターンキーの入力でキーボードが閉じるようになる理由
RxCocoa に限らず、.allEditingEvents をターゲットアクションに追加すると、リターンキーの入力によってキーボードが閉じるようになります。
次のようなコードで確認することが出来ます。
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 イベントが送信されないので、リターンキーをタップしてもキーボードは閉じません。
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 イベントが発行されず、キーボードを閉じることを抑止することができます。
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 イベントが発行され、かつキーボードが閉じるを抑止することができます。
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 を実装する