※ コメントでご指摘がありましたとおり、以下の内容は、UISearchController#searchResultsUpdaterを使えば、もっとうまくやれそうです。後日修正予定。
UISearchControllerが内包するsearchRetulsControllerから、pushViewControllerを呼んでも、画面が遷移しないことでハマる人がいると思います。
説明しやすいように以下のようにViewControllerに分類します
クラス | 役割 |
---|---|
MyContentsTableViewController | コンテンツの一覧を表示する。UINavigationControllerのrootViewControllerである |
UISearchController | 検索UIおよびMyContentsSearchResultControllerを内包する |
MyContentsSearchResultController | コンテンツの検索結果を表示する |
MyContentsTableViewControllerで以下のようにUISearchControllerを初期化していると思います。
// 実際はStoryboardから生成すると思いますが、説明のためにはしょってます
self.searchResultsController = MyContentsSearchResultController()
self.searchController = UISearchController(searchResultsController: searchResultsController)
searchController.searchBar.delegate = searchResultsController
searchController.dimsBackgroundDuringPresentation = true
searchController.delegate = searchResultsController
tableView.tableHeaderView = searchController.searchBar
このように初期化すると以下のような挙動をするようになります。
- UISearchController#searchBarが、MyContentsTableViewController#tableViewのヘッダに表示される
- searchController、searchBarのdelegateイベントは、MyContentsSearchResultControllerが受け取る
- searchBarをタップして入力状態(FirstResponder)になったとき、UISearchControllerがモーダル表示される
- 検索テキストを入力しない状態では、MyContentsTableViewControllerが透けて表示されている
- 検索テキストを入力すると、MyContentsSearchResultControllerが表示される
そして、MyContentsSearchResultControllerから以下のようにしても画面遷移しません。
self.navigationController?.pushViewController(controller, animated: true)
理由は明白で、self.navigationControllerがnilだからです。画面を操作しているとわかりにくいのですが、UISearchBarの入力領域をタップして、検索モードに入った時、UISearchControllerがモーダルとして表示されています。だからUISearchControllerも内包するsearchResultsControllerも、navigationControllerがnilなのは当たり前なのです。これらはUINavigationControllerのrootViewControllerではありませんから・・・
どのViewControllerからpushViewControllerを呼ぶべきか?
UISearchController呼び出し元のMyContentsTableViewControllerのpushViewControllerを呼ばべいいのです。そのためには、MyContentsSearchResultControllerでMyContentsTableViewControllerをプロパティで持てばいいでしょう。
let searchResultsController = MyContentsSearchResultController()
searchResultsController.contentsTableViewController = self
/// weakの方が良さげ。MyContentsTableViewControllerの方がインスタンス寿命が長いはずだから
weak var contentsTableViewController: MyContentsTableViewController?
---
self.contentsTableViewController.pushViewController(controller, animated: true)
しかし、ただpushViewControllerを呼ぶだけでは問題があります。先程、書いたようにUISearchControllerはモーダルとして表示されているため、以下の様なことが起きます。
ケース1。searchBarをタップし、まだテキスト入力していない場合。
この時、UISearchControllerはモーダル表示されていますが、まだMyContentsTableViewControllerが表示されています。
searchController.dimsBackgroundDuringPresentation = true
とした場合には、その表示領域はグレー表示になり、タップすると検索が解除されます。つまりUISearchControllerがdismissされモーダルでなくなる。さらに言うとUISearchControllerがモーダル中は、MyContentsTableViewControllerのセルをタップできない。
しかしdimsBackgroundDuringPresentationがfalseのときは、MyContentsTableViewControllerのtableViewのセルをタップできてしまいます。そして、MyContentsTableViewControllerのnavigationController.pushViewControllerが呼び出され次の画面に遷移します。
このとき、次の画面でNavigationBarが非表示になり、戻るボタンが押せません・・・
今、手元で確認できないが検索バーが表示されたままのこともあった気がする・・・
なぜ、そうなるかというとUISearchControllerがモーダル表示のままだからです。searchBarをタップするとNavigationBarが非表示になりますよね?その状態を引きずっているから、次の画面でも非表示のままなのです。
対処としては、pushViewControllerの前に、UISearchControllerをdismissするのが良さそうである。
self.searchController.dismiss(animated: true, completion: {
self.navigationController?.pushViewController(con, animated: true)
})
これで遷移先でNavigationBarが表示される。
ケース2。searchBarをタップし、テキスト入力した場合。
この時、UISearchControllerはモーダル表示され、かつ、MyContentsSearchResultsControllerが表示されています。先程書いたように画面遷移したい時は、以下のようにします。
self.contentsTableViewController?.pushViewController(controller, animated: true)
このとき、見た目上、画面遷移しません・・・
searchBarのキャンセルを押して、UISearchControllerをdismissすると、画面遷移しています。考えれば当たり前のことで、UISearchControllerがトップモーダルで表示されていて、バックグラウンドのMyContentsTableViewControllerで画面遷移しているからです。
つまり、この場合もUISearchControllerをdismissする必要があります。
// selfの親がUISearchControllerである
if let searchController = self.parent as? UISearchController {
searchController.dismiss(animated: true, completion: {
self.contentsTableViewController?.navigationController?.pushViewController(controller, animated: true)
})
}
遷移先の画面から戻ってきた時、また検索状態に戻す必要がある
これで画面遷移はできました。しかし、遷移先の画面から戻ってきた時、検索状態が解除されており、一覧表示も検索前の状態になっています。ユーザーからすると不自然に見えるでしょう。よって、viewWillAppearなどで検索状態に戻す必要があります。
以下のようにすると検索状態にすることができます。
self.searchController.isActive = true
ただ、これも単にviewWillAppearで呼ぶだけだと、最初の画面表示で検索状態になってしまうので、フラグを見て次の画面から戻ってきたときのみ呼ぶようにしなければいけません。
面倒ですね・・・
しかも、画面遷移するときも、戻ってきたときも、一瞬、検索前の状態(MyContentsTableViewController)が見えてしまい、あまり見栄えも良くないです。
結論として、自分は検索UIにUISearchControllerは使わないことにし、代わりに以下のように実装することにしました。
- MyContentsTableViewControllerで検索結果の表示機能も持つようにする
- UISearchBarを直接インスタンス生成し、それをUITableView.tableViewHeaderにひもづる
- MyContentsTableViewControllerがUISearchBarのdelegateイベントも受け取る
- 非検索用と検索用とでコンテンツの配列を分けて持つ
- var rows: [Contents], var searchResultRows: [Contents]のように
- 検索テキストが入力されたら、tableViewをreloadDataして、検索用のコンテンツ配列を参照して表示する
さよなら、UISearchController・・・