RxSwift/RxCocoaを使ってインクリメンタルサーチを実装する方法
iOSでインクリメンタルサーチしたい
今回はiOSでインクリメンタルサーチをしたいケースがあり、色々と調べたところRxSwift/RxCoccoaを使うと少ないコードで実現できるとわかったので ブログに残しておこうと思います。
まずはインクリメンタルサーチとはなんぞや?という方のために簡単な説明を。
インクリメンタルサーチ(incremental search)とは、アプリケーションにおける検索方法のひとつ。検索したい単語をすべて入力した上で検索するのではなく、入力のたびごとに即座に候補を表示させる。逐語検索、逐次検索とも。
Safariでも検索ワードを入れると確定ボタンをタップする前に即座に候補を表示してくれますね。あれです。
これをやりたかった
先に今回やりたかったインクリメンタルサーチのGIFを載せておきます。
画面上部にUISearchBarがあり、その下にUITableViewがあるシンプルな画面です。
ひらがなを入力したら確定ボタンをタップする前にデータをフィルタリングして表示しています。
検証環境
本エントリは以下の環境で検証を行っています。
- macOS Sierra バージョン 10.12.6
- Xcode Version 9.2 (9C40b)
- Swift 4
- iPhone X シミュレーター iOS 11.2
- CocoaPods 1.3.1
RxSwift/RxCocoaを導入
実装にあたり、RxSwift/RxCocoaが必要なのでインストールしておきましょう。 筆者はCocoaPodsでインストールしました。 以下はPodfileの例です。
target 'ターゲット名' do use_frameworks! pod 'RxSwift' pod 'RxCocoa' end
pod installしたところ、筆者の環境ではRxSwift、RxCocoa共にバージョン4.0.0
がインストールされました。
インクリメンタルサーチの実装
以下がViewControllerのコード全体です。全体だとそれなりのコード量ですが、
インクリメンタルサーチに関する部分はsetupSearchBar()
(ハイライト部分)だけです。
import UIKit import RxSwift import RxCocoa struct User { let name: String } class ViewController: UIViewController { @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var tableView: UITableView! private let allUsers = [ User(name: "かとう"), User(name: "たなか"), User(name: "ひらや"), User(name: "おおはし"), User(name: "やまもと"), User(name: "ふかさわ"), User(name: "さいとう"), User(name: "くどう"), User(name: "すわ"), User(name: "わたなべ") ] private var filteredUsers = [User]() private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setup() } } // MARK: - Private private extension ViewController { func setup() { filteredUsers = allUsers tableView.dataSource = self tableView.keyboardDismissMode = .onDrag setupSearchBar() } func setupSearchBar() { searchBar.delegate = self let debounceInterval = 0.3 // インクリメンタルサーチのテキストを取得するためのObservable let incrementalSearchTextObservable = rx // UISearchBarに文字列入力中に呼ばれるUISearchBarDelegateのメソッドをフック .methodInvoked(#selector(UISearchBarDelegate.searchBar(_:shouldChangeTextIn:replacementText:))) // searchBar.textの値が確定するまで0.3待つ .debounce(debounceInterval, scheduler: MainScheduler.instance) // 確定したsearchBar.textを取得 .flatMap { [unowned self] _ in Observable.just(self.searchBar.text ?? "") } // UISearchBarのクリア(×)ボタンや確定ボタンタップにテキストを取得するためのObservable let textObservable = searchBar.rx.text.orEmpty.asObservable() // 2つのObservableをマージ let searchTextObservable = Observable.merge(incrementalSearchTextObservable, textObservable) // 初期化時に空文字が流れてくるので無視 .skip(1) // 0.3秒経過したら入力確定とみなす .debounce(debounceInterval, scheduler: MainScheduler.instance) // 変化があるまで文字列が流れないようにする、つまり連続して同じテキストで検索しないようにする。 .distinctUntilChanged() // subscribeして流れてくるテキストを使用して検索 searchTextObservable.subscribe(onNext: { [unowned self] text in if text.isEmpty { // 空文字の場合は全件表示 self.filteredUsers = self.allUsers } else { // 入力文字列がある場合はデータをフィルタリングして表示 self.filteredUsers = self.allUsers.filter { $0.name.contains(text) } } self.tableView.reloadData() }).disposed(by: disposeBag) } } // MARK: - UITableViewDataSource extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return filteredUsers.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = filteredUsers[indexPath.row].name return cell } } // MARK: - UISearchBarDelegate extension ViewController: UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { return true } }
解説
コメントにも説明がありますが、setupSearchBar()
部分の実装ポイントを解説します。
methodInvoked
を使って、UISearchBarに文字を入力するたびに呼ばれるUISearchBarDelegateのsearchBar(_:shouldChangeTextIn:replacementText:)
メソッドをフックし、少し待つことでUISearchBarに入力中のテキストがsearchBar.text
として取得できる。- そのために
searchBar(_:shouldChangeTextIn:replacementText:)
を実装してtrue
を返す必要がある。
- そのために
- 確定ボタンやクリアボタンタップ時のUISearchBarのテキストを取得するためには
searchBar.rx.text
を使用する。 - 上記2つのObservableをマージして1つのObservableに。
- マージしたObservableをsubscribeしてテキストを取得して、検索。
おわりに
今回はRxSwift/RxCocoaを使ってインクリメンタルサーチを実装する方法をご紹介しました。
どなたかの参考になれば幸いです。