はじめに
https://qiita.com/doi_daihei/items/6e0330debab12bcde155
こちらの第二弾ですね。
今回もアプリ開発においてよくあるユーザー登録画面をRxで書いてみました。
ユーザー登録画面のフォーム制御ってUITextFieldのdelegateで書くと、むずかしいというより、面倒くさいですよね。
しかも、コンポーネント化しているとどこでどの制御しているか複雑になっちゃいますよね。
でもRxを用いれば、簡単かつ明瞭にかけますのでそちらをご紹介致します!
成果物
デザインはクソですが、
エラーメッセージの見せ方やsubmitボタンの活性/非活性の制御に関してはwebとかでもよく見受ける挙動ですね。
ソースコード
本来であれば、MVVMだとViewModelのinputに伝搬して...とかしないといけなかったり、通信したりなど色々抜けてはいますが
今回はあくまでフォームの制御ですので、そこに着目して書いていきます。
setup
setupに関しては、enumを用意し、
コンポーネント側に用意しておいた type
にbindしているだけです。
private func setup() {
Observable.just(.username).bind(to: usernameFormView.rx.type).disposed(by: disposeBag)
Observable.just(.email).bind(to: emailFormView.rx.type).disposed(by: disposeBag)
Observable.just(.password).bind(to: passwordFormView.rx.type).disposed(by: disposeBag)
Observable.just(.confirmPassword).bind(to: confirmPasswordFormView.rx.type).disposed(by: disposeBag)
}
制御
Rxで制御しているのは主に以下です。
- 個々のバリデーション結果をエラーメッセージのhiddenにbind
- 全てのバリデーション結果をsubmitボタンのenableにbind
ソースで書くと以下のような感じですね。
private func bind() {
let username = usernameFormView.rx.textFieldText.orEmpty.share(replay: 1) // share reply
let email = emailFormView.rx.textFieldText.orEmpty.share(replay: 1) // share reply
let password = passwordFormView.rx.textFieldText.orEmpty.share(replay: 1) // share reply
let confirmPassword = confirmPasswordFormView.rx.textFieldText.orEmpty.share(replay: 1) // share reply
// ユーザー名のエラーメッセージ
username
.filter { !$0.isEmpty }
.map { $0.isValidUsername() }
.bind(to: usernameFormView.rx.isCautionLabelHidden)
.disposed(by: disposeBag)
// メールアドレスのエラーメッセージ
email
.filter { !$0.isEmpty }
.map { $0.isValidEmail() }
.bind(to: emailFormView.rx.isCautionLabelHidden)
.disposed(by: disposeBag)
// パスワードのエラーメッセージ
password
.filter { !$0.isEmpty }
.map { $0.isValidPassword() }
.bind(to: passwordFormView.rx.isCautionLabelHidden)
.disposed(by: disposeBag)
// 確認パスワードのエラーメッセージ
Observable
.combineLatest(password, confirmPassword) { password, confirmPassword in
password == confirmPassword
}
.bind(to: confirmPasswordFormView.rx.isCautionLabelHidden)
.disposed(by: disposeBag)
// 全てのバリデーション判定をcombineLatestで最新精査
Observable
.combineLatest(username, email, password, confirmPassword) { username, email, password, confirmPassword in
username.isValidUsername() && email.isValidEmail() && password.isValidPassword() && (password == confirmPassword)
}
.bind(to: submitButton.rx.isEnabled)
.disposed(by: disposeBag)
}
コンポーネント->VCで制御するプロパティ
余談ではありますが、usernameFormView.rx.textFieldText
や emailFormView.rx.textFieldText
のようなコンポーネント側のRx系変数をVC側で用いたい場合が多々発生します。
その場合は以下のように書いておけば、内部で書いてあるRxの変数のように扱えます。
元々あるRxの変数のように扱えますので、パッと見でわかりますよね。
// MARK: - Reactive Extension
extension Reactive where Base: FormView {
var type: Binder<FormType> {
return Binder(base) { view, type in
view.titleLabel.text = type.title
view.cautionLabel.text = type.caution
view.textField.keyboardType = type.keyboardType
view.textField.isSecureTextEntry = type.isSecureTextEntry
view.textField.textContentType = type.textContentType
}
}
var textFieldText: ControlProperty<String?> {
return base.textField.rx.text
}
var isCautionLabelHidden: Binder<Bool> {
return base.cautionLabel.rx.isHidden
}
}
まとめ
本業の方のプロジェクトがそろそろ終わりそう(絶賛炎上中ですがw)なので、
次のプロジェクトではRxを布教して行けたらなと思います。
以下、ソースコードです。