iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

RxSwift で簡易インクリメンタルサーチ

個人で開発中のアプリで RxSwift を使用した簡易インクリメンタルサーチを実装したので、その方法を簡単にまとめたいと思います🙃

作業していくっ🧑🏻‍💻

今回行うインクリメンタルサーチの仕様は下記の通りで、入力した ID が使用可能かどうかを調べるためにこの機能を実装しています。

  • 入力があって1秒間次の入力がなかったら Firestore にリクエス
  • 同じ入力値の場合イベントを流さない
  • リクエスト中に入力があった場合は、リクエストをキャンセル
  • ローディング、成功、失敗のアイコンをそれぞれのステータスごとに出し分ける

完成イメージ

width=100

それでは、実際にコードを見ていきましょう。

    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 の debouncedistinctUntilChanged で行います。debounce は、連続したイベントを特定のインターバルにしたがってフィルターします。今回の場合は1秒に設定しているので、テキストの入力があってから1秒間の間に次の入力がなかった場合に、イベントを流します。distinctUntilChanged では、連続して同じ値が流れないためのフィルターを行なっています。RxSwift で、見慣れないオペレータがあれば下記の記事で丁寧に日本語でまとめっているので、大体のことは分かるかと思います。

qiita.com

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()

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com