RxSwiftを使って複数のUITextFieldの状態変化にバリデーションを実装する
概要
今回はRxSwiftを使って3つのラベルのの文字に制限を加えてその制限を満たしたらUIButtonをタップできるようにする実装を行います。 俗にいうバリデーション機能です。 使えそうなところは、
- メッセンジャーのメッセージの送信時のチェック
- ログイン時のバリデーションチェック
- ユーザー登録時の住所や名前、電話番号などの入力有無についてのチェック
などが代表例ですね。
開発環境について
Xcode: 10.1
Swift: 4.2
RxSwift: 4.4.0
RxCocoa: 4.4.0
storyboardについて
今回は3つのラベルの状態を監視するのでUILabel
は3つ
そのラベルの編集用にUITextField
を3つ
そして、ボタンを1つ
これらの部品をstoryboard に配置します。
配置はこのような感じになります。 @IBOutlet接続するのはUILabel3つとUITextField3つとUIButtonでそれぞれ接続させます。
そのため、ViewController.swiftのコードは次のようになります。
ViewController.swift
import UIKit import RxCocoa import RxSwift class ViewController: UIViewController { @IBOutlet weak var firstNameLabel: UILabel! @IBOutlet weak var firstNameTextField: UITextField! @IBOutlet weak var lastNameLabel: UILabel! @IBOutlet weak var lastNameTextField: UITextField! @IBOutlet weak var phoneNumberLabel: UILabel! @IBOutlet weak var phoneNumberTextField: UITextField! @IBOutlet weak var button: UIButton! var disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() self.button.isEnabled = false self.button.setTitle("押せません", for: .disabled) self.button.setTitle("押せます", for: .normal) // firstNameのバリデーション用変数です。 Observable<Bool>なので 中身のvalueがbool型です。 let firstNameValid: Observable<Bool> = firstNameTextField.rx.text .map{ text -> Bool in text?.count ?? 0 >= 5 // 5文字以上であれば trueを返す } .share(replay: 1) // テキストを1文字入力すると1度だけmapの処理が走ります。 // lastNameのバリデーション用変数です。 Observable<Bool>なので 中身のvalueがbool型です。 let lastNameValid = lastNameTextField.rx.text .map { text -> Bool in text?.count ?? 0 >= 5 // 5文字以上であれば trueを返す } .share(replay: 1) // phoneNumberのバリデーション用変数です。 Observable<Bool>なので 中身のvalueがbool型です。 let phoneNumberValid = phoneNumberTextField.rx.text .map { text -> Bool in text?.count ?? 0 >= 5 // 5文字以上であれば trueを返す } .share(replay: 1) // 3つのバリデーション変数(Observable<Bool>)を組み合わせる Observable.combineLatest(firstNameValid.asObservable(), lastNameValid.asObservable(), phoneNumberValid.asObservable()) .subscribe(onNext: { firstOk, lastOk, phoneOk in self.button.isEnabled = firstOk && lastOk && phoneOk }) .disposed(by: disposeBag) firstNameTextField.rx.controlEvent(.editingDidEndOnExit).asDriver() .drive(onNext: { _ in print("editingDidEndOnExit") self.firstNameTextField.resignFirstResponder() }) .disposed(by: disposeBag) lastNameTextField.rx.controlEvent(.editingDidEndOnExit).asDriver() .drive(onNext: { _ in print("editingDidEndOnExit") self.lastNameTextField.resignFirstResponder() }) .disposed(by: disposeBag) phoneNumberTextField.rx.controlEvent(.editingDidEndOnExit).asDriver() .drive(onNext: { _ in print("editingDidEndOnExit") self.phoneNumberTextField.resignFirstResponder() }) .disposed(by: disposeBag) } }
ちなみに3つのバリデーション用の変数を作るための.share(replay: 1)
の返り値はRxSwift.Observable<Self.E>
となります。
簡単に言えば、Observableです。
今回のバリデーションの実装で重要な概念が2つですね。
Valid.swift
// firstNameのバリデーション用変数です。 Observable<Bool>なので 中身のvalueがbool型です。 let firstNameValid: Observable<Bool> = firstNameTextField.rx.text .map{ text -> Bool in text?.count ?? 0 >= 5 // 5文字以上であれば trueを返す } .share(replay: 1) // テキストを1文字入力すると1度だけmapの処理が走ります。
と
// 3つのバリデーション変数(Observable<Bool>)を組み合わせる Observable.combineLatest(firstNameValid.asObservable(), lastNameValid.asObservable(), phoneNumberValid.asObservable()) .subscribe(onNext: { firstOk, lastOk, phoneOk in self.button.isEnabled = firstOk && lastOk && phoneOk }) .disposed(by: disposeBag)
この二つです。これらを3回くらい写経したらそのまま寝てしまってもいいくらいです。
RxSwiftはよくストリームとして「流れ」がある実装ができることが知られていますが、 僕は最初はこの流れと言うものがよく分かっていませんでした。
今、当時分かっていなかった自分に対して説明をするのならば、
この流れと言うのはSwiftのOptional
みたいなものだよと伝えていたと思います。
ストリームであるRxSwiftの中に機能(実装)を乗せたければObservable
で包まれた型
を作ればいいのです。
Observable
が観察可能
とかいう意味不明な言い方をしていますがObservable
は流れ
と一緒なのです。
このObservable
で包まれた型を使えばRx(リアクティブ)な実装ができるようになります。
そして、
Observable.combineLatest(firstNameValid.asObservable(), lastNameValid.asObservable(), phoneNumberValid.asObservable())
のcombineLatest
の部分がRxSwiftの実装になりこれは「組み合わせる」と言う意味にあります。
流れ的には
RxSwift -> combineLatest -> subscribe -> disposed
と言う流れになります。
上記の実装で複数のラベルのバリデーションを入力の都度確認してUIButtonの活性・不活性を制御できるようになりました。
以上で、基本的なRxSwiftの使い方は理解できるかなと思います。