RxSwift学びの過程〜エラーハンドリング編〜

#背景
最近はエラーハンドリングしなきゃしなきゃ病から`enum`で`Error`をやたら分けたようと多用しています。その過程でつまづいたところがあったので記します。
知識レベルのメンテナンスというか、しばらく触れずに忘れてしまうとまた取り返しがつかなくなってしまいますのでアプリを丸っと`RxSwift`で実装して、たまに覗いてメンテしてます。

# サンプルコード
内容は、テキストフィールドの入力が確認できれば`UIButton`が有効化するというもの。
また、入力がない場合は無効化するというRxの **THE 使い所** な部分

**view**

```swift
    private lazy var viewMdoel = ViewModel(
        itemLabelObservable: itemTextField.rx.text.asObservable(),
        detailLabelObservable: detailTextField.rx.text.asObservable(),
        tagLabelObservable: tagTextField.rx.text.asObservable(),
        memoLabelObservable: memoTextView.rx.text.asObservable(),
        addButtonTapped: addButton.rx.tap.asObservable(),
        validation: Validation()
    )

    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindingViewModel()
    }

    func bindingViewModel() {
        itemTextField.rx.text
            .bind(to: viewMdoel.itemLabel)
            .disposed(by: disposeBag)
        
        detailTextField.rx.text
            .bind(to: viewMdoel.detailLabel)
            .disposed(by: disposeBag)
        
        tagTextField.rx.text
            .bind(to: viewMdoel.tagLabel)
            .disposed(by: disposeBag)
        
        viewMdoel.isEnableButton
            .bind(to: addButton.rx.isEnabled)
            .disposed(by: disposeBag)

        addButton.rx.tap
            .subscribe(onNext: { _ in
                self.viewMdoel.addItem()
            })
            .disposed(by: disposeBag)   
    }
}

```


**viewModel**

```swift
final class AddTabViewModel {
    
    let validatedItem: Observable<Bool>
    let validatedDetail: Observable<Bool>
    let validatedTag: Observable<Bool>
    let isEnableButton: Observable<Bool>
    let itemLabel = BehaviorRelay<String?>(value: "")
    let detailLabel = BehaviorRelay<String?>(value: "")
    let tagLabel = BehaviorRelay<String?>(value: "")
    let memoLabel = BehaviorRelay<String?>(value: "")
    
    init(
        itemLabelObservable: Observable<String?>,
        detailLabelObservable: Observable<String?>,
        tagLabelObservable: Observable<String?>,
        memoLabelObservable: Observable<String?>,
        addButtonTapped: Observable<Void>,
        validation: TextValidate
        ) {
       
        validatedItem = itemLabelObservable
            .flatMap { (input) -> Observable<Bool> in
                return validation
                    .validate(text: input)
            }
            .share()
        
        validatedDetail = detailLabelObservable
            .flatMap { (input) -> Observable<Bool> in
                return validation
                    .validate(text: input)
            }
            .share()
        
        validatedTag = tagLabelObservable
            .flatMap { (input) -> Observable<Bool> in
                return validation
                    .validate(text: input)
            }
            .share()
        
        isEnableButton = Observable.combineLatest(
            validatedItem,
            validatedDetail,
            validatedTag
            
        ) {
            $0 && $1 && $2
        }
        .share()
    }
    
    func addItem() {
        let item = Item(
            name: itemLabel.value!,
            detail: detailLabel.value!,
            tag: tagLabel.value!,
            memo: memoLabel.value!,
            fav: false,
            celllNo: items.count
        )
        items.append(item)
    }
}

```

**validation**

```swift
enum TextError: Error {
    case blank
    case length
}

protocol TextValidate {
    func validate(text: String?) -> Observable<Bool>
}

final class Validation: TextValidate {
    
    func validate(text: String?) -> Observable<Bool> {
        switch text {
        case .none:
            return Observable.error(TextError.blank)
        case let text?:
            switch text.isEmpty {
            case true:
                return Observable.error(TextError.blank)
            case false:
                return Observable.just(true)
            }
        }
    }
}

extension TextError {
    var button: Bool {
        switch self {
        case .blank, .length:
            return false
        }
    }
}
```

## 問題
上記コードでは、入力されたテキストの状態を`validate`し、結果を`Bool`で返す流れ。その値によって有効化を切り替えますが、ここでの`Error Enum`によって後々フローを分けようかなと考えていました。

binding自体はされていると思ったのですが、このままビルドすると落ちてしまいます。
`RxSwift`内に以下のメッセージが出ます。
`Thread 1: Fatal error: Binding error: blank`

## 原因
bindingはできているっぽくて、フローも問題なさそうなのにどこでエラーが起こっているのか。
原因は`enum TextError: Error`にてエラーをストリームに流しており、UIがこのエラーを受け取ってしまっているから。UIは`Error`を受け取れません。

現状の対策として、`Error`をまず流さないようにする = `Error`は`false`に変えてあげることで対応します。

## 対策
どこで変えればいいか。この場合だと以下の３つほど使えそうです。

```swift
// `func validate()`の中

func validate(text: String?) -> Observable<Bool> {
        switch text {
        case .none:
            //return Observable.error(TextError.blank)
          return Observable.just(false) //とか
        case let text?:
            switch text.isEmpty {
            case true:
                //return Observable.error(TextError.blank)
              return Observable.just(false) //とか
            case false:
                return Observable.just(true)
            }
        }
    }

//--------------------------------------------------------

// ストリームを作っているフローの`func validate()`の直後

validatedItem = itemLabelObservable
    .flatMap { (input) -> Observable<Bool> in
        return validation
            .validate(text: input)
            .catchErrorJustReturn(false) //とか
    }
    .share()

//--------------------------------------------------------

// ストリームを`combineLatest`で繋げ、全ての処理が終了した直後

isEnableButton = Observable.combineLatest(
    validatedItem,
    validatedDetail,
    validatedTag

) {
    $0 && $1 && $2
}
.catchErrorJustReturn(false) //とか
.share()
```

結論から言うと、最後の３つめは実装すると落ちなくはなりますが`button`が無効化のままです。

なぜか。それは`error`および`completed`は**流れた後一切の後続のものが流れない**と言う前提があります。
つまり、実装のデフォルト状態でまず`false`になっているので、このまま後は流れないと言うことになります。

よって上二つが希望の動作をしてくれます。
どちらでも問題は解消されますが、後者の方が汎用性はありそうです。

なぜ`validate`で`Error`を返していたのかというと、`Observable<Error>`で返ったものを`.materialise`して〜などといったものを今後行おうと思っていたためです。
  
  ちなみに`.catchErrorJustReturn(false)`は、`Error`を`catch`したら`Just Bool(false) returnするぜ`というとってもpureなenglishで分かやすいものです。
  
# まとめ
  ただ`Bool`を返すだけだったら、変にハンドリングするコードを作成せずシンプルにした方がいいかもしれません。
  
  `enum`で`Error`を作成して`case`を分けて〜とするとコードの可読性も上がっていいですが、特に必要なさそうだったらコード量の増加を招くので適材適所ということで。
  
`Error`はそのままUIに渡してはいけません。ちゃんと最後まで面倒を見ましょう。

  
# 補足
  上記コードに更に指摘していただたい部分として、
  最新値だけ取得したいなら　→　`.flatMapLatest`
  これらのような問題を回避するため　→　`Driver` `Relay`
  を活用すればという意見もいただきました。