RxSwift で簡易インクリメンタルサーチ
個人で開発中のアプリで RxSwift を使用した簡易インクリメンタルサーチを実装したので、その方法を簡単にまとめたいと思います🙃
作業していくっ🧑🏻💻
今回行うインクリメンタルサーチの仕様は下記の通りで、入力した ID が使用可能かどうかを調べるためにこの機能を実装しています。
- 入力があって1秒間次の入力がなかったら Firestore にリクエスト
- 同じ入力値の場合イベントを流さない
- リクエスト中に入力があった場合は、リクエストをキャンセル
- ローディング、成功、失敗のアイコンをそれぞれのステータスごとに出し分ける
完成イメージ
それでは、実際にコードを見ていきましょう。
func bind() {
incrementalSearchTextRelay.asObservable()
.debounce(.seconds(1), scheduler: MainScheduler.instance)
.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()
Discussion