質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
RxSwift

RxSwiftは、Reactive ExtensionsのSwift向けの実装です。iOS開発に用いられ、リアクティブプログラミングを可能にします。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

3194閲覧

Swift5 RxSwift 文字入力のバリデーションからのボタンの有効化

mogiruri

総合スコア37

RxSwift

RxSwiftは、Reactive ExtensionsのSwift向けの実装です。iOS開発に用いられ、リアクティブプログラミングを可能にします。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2019/06/19 00:46

編集2019/06/19 03:38

こんにちは。

現在RxSwiftの勉強をしており、テキスト入力のバリデーションをしてボタンを有効化するという流れを作っています。
ですが以下のコードを実装すると落ちてしまいます。

View

swift

1import UIKit 2import RxCocoa 3import RxSwift 4 5class AddTabViewController: UIViewController { 6 7 @IBOutlet weak var titleLable: UILabel! 8 @IBOutlet weak var itemTextField: UITextField! 9 @IBOutlet weak var detailTextField: UITextField! 10 @IBOutlet weak var tagTextField: UITextField! 11 @IBOutlet weak var memoTextView: UITextView! 12 @IBOutlet weak var addButton: UIButton! 13 14 private lazy var viewMdoel = AddTabViewModel( 15 itemLabelObservable: itemTextField.rx.text.asObservable(), 16 detailLabelObservable: detailTextField.rx.text.asObservable(), 17 tagLabelObservable: tagTextField.rx.text.asObservable(), 18 memoLabelObservable: memoTextView.rx.text.asObservable(), 19 addButtonTapped: addButton.rx.tap.asObservable(), 20 validation: Validation() 21 ) 22 23 private let disposeBag = DisposeBag() 24 25 override func viewDidLoad() { 26 super.viewDidLoad() 27 bindingViewModel() 28 } 29 30 func bindingViewModel() { 31 32 itemTextField.rx.text 33 .bind(to: viewMdoel.itemLabel) 34 .disposed(by: disposeBag) 35 36 detailTextField.rx.text 37 .bind(to: viewMdoel.detailLabel) 38 .disposed(by: disposeBag) 39 40 tagTextField.rx.text 41 .bind(to: viewMdoel.tagLabel) 42 .disposed(by: disposeBag) 43 44 memoTextView.rx.text 45 .bind(to: viewMdoel.memoLabel) 46 .disposed(by: disposeBag) 47 48 // ここで落ちているかと思われます 49 viewMdoel.isEnableButton 50 .bind(to: addButton.rx.isEnabled) 51 .disposed(by: disposeBag) 52 53 addButton.rx.tap 54 .subscribe(onNext: { _ in 55 self.viewMdoel.addItem() 56 }) 57 .disposed(by: disposeBag) 58 } 59} 60

ViewModel

swift

1 2import Foundation 3import RxSwift 4import RxCocoa 5 6final class AddTabViewModel { 7 8 let validatedItem: Observable<Bool> 9 let validatedDetail: Observable<Bool> 10 let validatedTag: Observable<Bool> 11 let isEnableButton: Observable<Bool> 12 let itemLabel = BehaviorRelay<String?>(value: "") 13 let detailLabel = BehaviorRelay<String?>(value: "") 14 let tagLabel = BehaviorRelay<String?>(value: "") 15 let memoLabel = BehaviorRelay<String?>(value: "") 16 17 init( 18 itemLabelObservable: Observable<String?>, 19 detailLabelObservable: Observable<String?>, 20 tagLabelObservable: Observable<String?>, 21 memoLabelObservable: Observable<String?>, 22 addButtonTapped: Observable<Void>, 23 validation: TextValidate 24 ) { 25 26 27 validatedItem = itemLabelObservable 28 .flatMap { (input) -> Observable<Bool> in 29 return validation 30 .validate(text: input) 31 } 32 .share() 33 34 validatedDetail = detailLabelObservable 35 .flatMap { (input) -> Observable<Bool> in 36 return validation 37 .validate(text: input) 38 } 39 .share() 40 41 validatedTag = tagLabelObservable 42 .flatMap { (input) -> Observable<Bool> in 43 return validation 44 .validate(text: input) 45 } 46 .share() 47 48 isEnableButton = Observable.combineLatest( 49 validatedItem, 50 validatedDetail, 51 validatedTag 52 ) { 53 $0 && $1 && $2 54 } 55 .share() 56 57 } 58 59 func addItem() { 60 let item = Item( 61 name: itemLabel.value!, 62 detail: detailLabel.value!, 63 tag: tagLabel.value!, 64 memo: memoLabel.value!, 65 fav: false, 66 celllNo: items.count 67 ) 68 items.append(item) 69 } 70 71} 72 73

実行したい流れは、
3つのインプットが確認できればボタンを有効かするというものなのですが、
combineLatestの部分が間違ってるのでしょうか?

.validateではBoolを返すようにしており、
inputのtextFieldの3つがtrueならば、isEnableがtrueになるという風に
イメージして書いてみたのですが、Rxにまだ不慣れのため手こずっています。
どこに不備があるのか指摘していただけないでしょうか?

一応いかにValidationのコードも記載いたします。

よろしくお願いいたします。

Validation Model

swift

1import Foundation 2import RxSwift 3 4enum TextError: Error { 5 case blank 6 case length 7} 8 9protocol TextValidate { 10 func validate(text: String?) -> Observable<Bool> 11} 12 13final class Validation: TextValidate { 14 15 func validate(text: String?) -> Observable<Bool> { 16 switch text { 17 case .none: 18 return Observable.error(TextError.blank) 19 case let text?: 20 switch text.isEmpty { 21 case true: 22 return Observable.error(TextError.blank) 23 case false: 24 return Observable.just(true) 25 } 26 } 27 } 28} 29 30extension TextError { 31 var button: Bool { 32 switch self { 33 case .blank, .length: 34 return false 35 } 36 } 37} 38 39

追記

出現しているエラーメッセージです。

Thread 1: Fatal error: Binding error: blank

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

fuzzball

2019/06/19 01:19

エラーメッセージは?
mogiruri

2019/06/19 03:39

コメントありがとうございます。 エラーメッセージ追加いたしました。
guest

回答1

0

ベストアンサー

クラッシュの原因は、Errorを流すストリームをそのままUIにバインドしてしまっていることです。UIはErrorを受け取ることはできませんから、Error時の代わりの値なり、分岐なりを設定してあげる必要があります。

とりあえず、Errorが流れてきたらボタンを無効にしたいということであれば、以下のように修正すればよいでしょう。

swift

1validatedItem = itemLabelObservable 2 .flatMapLatest { (input) -> Observable<Bool> in 3 return validation 4 .validate(text: input) 5 .catchErrorJustReturn(false) // 後続の処理にエラーを流さないために、ここでcatchします 6 } 7 .share() 8 9// 以下同様...

※本件とは関係ないですが、itemLabelObservableに流れてきた最新の値についてのみバリデーションを行えばいいはずなので、flatMapではなくflatMapLatestの方を使うべきかと思います。

また、このようなミスを防ぐ手段として、DriverやRelayといったものがRxCocoaには用意されています。これらの使用も検討してみると良いかもしれません。

投稿2019/06/19 05:04

編集2019/06/19 08:13
kakajika

総合スコア3131

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mogiruri

2019/06/19 07:22

コメントありがとうございました。 .cathErrorを間に挟んだところ落ちることはなくなりました。エラーハンドリングは最後まで書き切っていなかったので納得いきました。 ですが、テキスト入力後にボタンがdisableのままで有効化されません。 見た所、バインディング自体はされているように見えますが、3つのtrueを確認できたらというフローが機能していないのでしょうか? 何かご指摘できるところがあれば是非お願いいたします。
kakajika

2019/06/19 08:15 編集

そうでした、すみません。 Rxのストリームの基本的な性質として、Errorを流した後は一切の値を流さない、というものがあります。そもそもErrorは本来、望ましくない状態に陥った場合に発生させるものですから、今回の場合はボタンを無効にするためにErrorを使うべきではなく、単純にtrue/falseを流してあげればいいはずです。 回答の方も修正しておきます。 →と思ったのですが、あまりコードの方針に口出しするのもどうかと思ったので、最低限の修正にとどめておきました。
mogiruri

2019/06/19 08:21

ご回答ありがとうございます。 observable.just(false)にしてみたところ無事に期待通りの動きをしてくれました。 errorとcompletedでdisposedになるという意味がいまいち掴めてなかったのですが、このようにsubscribeができなくなるんですね。今回のような継続して動作を切り替えたい場合は不適切と、、、 最近しっかりエラーハンドリングしなきゃとどんな場面にも詰め込もうとする部分があったので、このようにやらなくてもいい部分が見えて良かったです。 提案していただいたflatMapLatest, Driver, Relayも調べて採用していきたいと思います。 Rxは勉強のステップが踏みづらくてとても大変です。 ありがとうございました。
mogiruri

2019/06/19 08:28

※エラーハンドリングをしなくていい部分というより、ハンドリングでエラーをここで流すかどうかが問題ですね ※修正ありがとうございます。validation内でfalseを流すというより、確かにvalidation内ではerrorHandlingをきっちり行ってもいいが、ご指摘の場所でerrorを捕まえたらfalseを返すというフローの方が汎用性も良さそうです。
kakajika

2019/06/19 10:51

おっしゃる通りで、意図を汲んでいただけてよかったです。 バリデーションの結果をErrorで返すのが適切かどうかは正直微妙なところだと思います。失敗の際に理由も含めて流したいという意図があるのだと思いますが、Result型のようなものを使うのがより望ましいと思います。(たしかSwift5から使えたような?)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問