LoginSignup
19
15

More than 5 years have passed since last update.

UISearchControllerのインクリメンタルサーチをよりインクリメンタルにする

Posted at

iOS では UISearchController を使用することで、ナビゲーションバーへの検索フィールドの統合や、入力イベントに応じた検索処理の実施と結果の表示といったインクリメンタルサーチの流れの実装が非常に楽になります。ただ、私はそのインクリメンタルサーチの動作に 違和感 がありました。

インクリメンタルサーチのサンプル

インクリメンタルサーチの違和感について言及するためのサンプルコードについて説明します。

メイン画面

次のコードは果物をリスト表示するメイン画面 MainViewController の実装です。ここに UISearchController による検索バーが組み込まれています。

  • UISearchController の検索結果画面として後述の ResultViewController を登録してあります
  • UISearchResultsUpdating をトリガーに入力されたキーワードを含む くだもの を検索して、結果画面に表示させます
MainViewController.swift
import UIKit

class MainViewController: UITableViewController, UISearchResultsUpdating {

    private let items: [String] = [
        "🍎りんご", "🍊みかん", "🍇ぶどう", "🍒さくらんぼ",
        "🍓いちご", "🍉スイカ", "🍌バナナ", "🍏あおりんご",
        "🍐なし", "🍋レモン", "🍈メロン", "🍑もも",
        "🥭マンゴー", "🍍パイナップル", "🥥ココナッツ", "🥝キウイ",
    ]

    private var searchController: UISearchController!
    private var resultsController: ResultsViewController!

    override func viewDidLoad() {
        super.viewDidLoad()

        definesPresentationContext = true

        resultsController = ResultsViewController()

        searchController = UISearchController(searchResultsController: resultsController)
        searchController.dimsBackgroundDuringPresentation = true
        searchController.hidesNavigationBarDuringPresentation = true
        searchController.searchResultsUpdater = self

        navigationItem.hidesSearchBarWhenScrolling = false
        navigationItem.searchController = searchController

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    func updateSearchResults(for searchController: UISearchController) {
        if let keyword = searchController.searchBar.text, !keyword.isEmpty {
            resultsController.items = items.filter { $0.contains(keyword) }
        } else {
            resultsController.items = []
        }
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = items[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = item
        return cell
    }

}

結果画面

次のコードは果物を検索した結果を表示する結果画面 ResultViewController の実装です。メイン画面から受け取った結果を表示するだけです。

ResultViewController
class ResultsViewController: UITableViewController {

    var items: [String] = [] {
        didSet {
            if isViewLoaded {
                tableView.reloadData()
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = items[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = item
        return cell
    }

}

実行結果

このサンプルコードの実行した結果が次の動画です。

before.gif

インクリメンタルサーチの違和感の正体

上記の動画で私が感じた 違和感 の正体は、検索キーワードの入力が変換中の場合には、検索結果が表示されていないところです。デバッグしてみると、UISearchResultsUpdatingupdateSearchResults(for:) は変換中には呼び出されていません。

インクリメンタルじゃない!

そこで私が検索機能をよく利用する iOS 標準アプリ App Store と iTunes を確認してみたところ、二つとも変換中の入力でも検索結果が表示されました(インクリメンタル!)。なので、私の中での標準的な動きはこれらのアプリの動作だったのです。

appstore.gif

インクリメンタルサーチをよりインクリメンタルにする

ということで、App Store や iTunes アプリのように、よりインクリメンタルなインクリメンタルサーチを UISearchController で実装してみます。UISearchController にこれを実現するようなオプション等がないため、結局のところ UISearchBarDelegatesearchBar(_:, shouldChangeTextIn:, replacementText:) イベントを拾って実現するしかありませんでした。

つまり、下記のように修正します。

MainViewController.swift
//class MainViewController: UITableViewController, UISearchResultsUpdating {
class MainViewController: UITableViewController, UISearchBarDelegate {
MainViewController.swift
        //searchController.searchResultsUpdater = self
        searchController.searchBar.delegate = self
MainViewController.swift

//    func updateSearchResults(for searchController: UISearchController) {
//        if let keyword = searchController.searchBar.text, !keyword.isEmpty {
//            resultsController.items = items.filter { $0.contains(keyword) }
//        } else {
//            resultsController.items = []
//        }
//    }

    func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        // searchResultsController を強制的に表示する
        // (検索バーの日本語変換待ち状態の入力のみだと、searchResultsController 自体が表示されないため)
        searchController.searchResultsController?.view.isHidden = false

        // 変換中のテキストも正しく取得できるようにするために遅延させる
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
            guard let self = self else { return }
            if let keyword = searchBar.text, !keyword.isEmpty {
                self.resultsController.items = self.items.filter { $0.contains(keyword) }
            } else {
                self.resultsController.items = []
            }
        }

        return true
    }

ポイントとしては、検索バーに変換中の入力しかない状態だと、searchResultsController 自体が表示されないため、強制的に表示するようにしています。

また、searchBar(_:, shouldChangeTextIn:, replacementText:) には SearchBarに入力中の文字を使い、リストから候補を探したい場合(インクリメンタルサーチ) の記事にある通り、変換中の文字列を正しく判断できないという問題があるため回避策を入れてあります。

after.gif

はい、よりインクリメンタルなインクリメンタルサーチになりました!

さいごに

この記事を書いてから、ふと気になって他の iOS 標準アプリをいくつか(メッセージ、連絡先、Apple Store, iTunes Storeなど)確認してみたのですが、それらは UISearchController と同じ動作になっていました。たまたま私がよく検索を利用するアプリだけが特別対応されていたということです。なんと。

19
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
15