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 に配置します。

f:id:qed805:20190211185936p:plain
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の使い方は理解できるかなと思います。