個人で開発中のアプリで RxSwift を使用した簡易インクリメンタルサーチを実装したので、その方法を簡単にまとめたいと思います🙃
作業していくっ🧑🏻💻
今回行うインクリメンタルサーチの仕様は下記の通りで、入力した ID が使用可能かどうかを調べるためにこの機能を実装しています。
- 入力があって1秒間次の入力がなかったら Firestore にリクエスト
- 同じ入力値の場合イベントを流さない
- リクエスト中に入力があった場合は、リクエストをキャンセル
- ローディング、成功、失敗のアイコンをそれぞれのステータスごとに出し分ける
完成イメージ
それでは、実際にコードを見ていきましょう。
func bind() { incrementalSearchTextRelay.asObservable() .debounce(.seconds(1), scheduler: MainScheduler.instance) /* 1秒間イベントがなかったら最後の値を流す */ .distinctUntilChanged() /* 同じ値が連続して流れてきた場合イベントを流さない */ .flatMap{[unowned self] id in self.presenter.validationId(id: id) } /* ID のバリデーション */ .subscribe(onNext: {[weak self] result in switch result { case .success(let isEnable): if isEnable, let isEnableSavedText = self?.presenter.isEnableNext { self?.nextButton.isEnabled = isEnableSavedText } self?.errorLabel.text = "このアカウントIDは現在使用されています。" self?.errorLabel.isHidden = isEnable self?.accountIdTextField.updateAccountIdSearchState(state: isEnable ? .success : .error) case .error(let error): self?.errorLabel.text = error.localizedDescription self?.errorLabel.isHidden = false self?.accountIdTextField.updateAccountIdSearchState(state: .error) } }) .disposed(by: disposeBag) }
まずは、incrementalSearchTextRelay
を Observe してテキストの入力イベントを受け取り、インクリメンタルサーチの肝である、イベントのフィルターを RxSwift の debounce
と distinctUntilChanged
で行います。debounce
は、連続したイベントを特定のインターバルにしたがってフィルターします。今回の場合は1秒に設定しているので、テキストの入力があってから1秒間の間に次の入力がなかった場合に、イベントを流します。distinctUntilChanged
では、連続して同じ値が流れないためのフィルターを行なっています。RxSwift で、見慣れないオペレータがあれば下記の記事で丁寧に日本語でまとめっているので、大体のことは分かるかと思います。
private let incrementalSearchTextRelay = PublishRelay<String>.init() ... incrementalSearchTextRelay.accept(id)
フィルターを通してイベントが流れてきたら、.flatMap{}
で self.presenter.validationId(id: id)
を実行し、ID のバリデーションを行います。ちなみに、presenter.validationId(id: id)
は下記のように実装されています。なぜ、enum で error をラップしているかといいますと、取得した error をそのまま、observer.onError(error)
のように流してしまうと、dispose
が呼ばれて Observable の購読が終了してしまうからです。購読が終了して、イベントを受信できなくなるのを防ぐために今回は error を FideeResult としてラップしています。
func saveFideeId(id: String) -> Observable<FideeResult<Bool>> enum FideeResult<T> { case success(T) case error(Error) }
そして最後に流れてきた値をもとに、UI を更新すれば簡易インクリメンタルサーチの完了です🎉
余談
リクエスト中に次のイベントが発生した時のために、validationId(id:)
の処理の最初に、Current のリクエストをキャンセルしています。(書くの忘れそうになってました)
func validationId(id: String) -> Observable<FideeResult<Bool>> { currentValidationDisposables?.dispose()