Build a Query Suggestions UI with InstantSearch iOS and UIKit
Build a user interface to show Query Suggestions in your InstantSearch iOS app.
When your user interacts with a search box,
you can help them discover what they could search for by providing Query suggestions.Query suggestions are a specific kind of multi-index interface:
To display the suggestions in your iOS app, use Algolia’s MultiHitsViewModel component. Read on for an example of how to display a search bar with instant results and query suggestions “as you type”.
The initial screen shows the search box and results for an empty query:When users tap the search box, a list of query suggestions are shown (the most popular for an empty query):On each keystroke, the list of suggestions is updated:When users select a suggestion from the list, it replaces the query in the search box, and the suggestions view controller disappears.
The results view controller presents search results for the selected suggestion:
A basic query suggestions view controller is provided out-of-box with the InstantSearch library.
The query suggestions view controller is a hits view controller that uses the QuerySuggestion object provided by the InstantSearch Core library as a search record.
ProductTableViewCell is a subclass of UITableViewCell for visually displaying store items in the list of results.
This view uses the SDWebImage library for asynchronous image loading. To use ProductTableViewCell, you need to add the SDWebImage library to your project.
Algolia doesn’t provide a ready-to-use results view controller, but you can create one with the tools in the InstantSearch library by copying and pasting the following code to your project.
Add StoreItemsTableViewController, a subclass of UITableViewController, which implements the HitsController protocol. This view controller presents the search results using the previously declared ProductTableViewCell.
The suggestion model is provided by InstantSearch iOS and called QuerySuggestion.
Define the SearchSuggestionTableViewCell displaying a search suggestion.
Define the SuggestionsTableViewController, a subclass of UITableViewController implementing HitsController protocol which displays the list of suggestions.
As a selection of a suggestion or type ahead button click might trigger the textual query change, this view controller also conform to SearchBoxController protocol, so it can be easily connected to SearchBoxInteractor or SearchBoxConnector.
Swift
Report incorrect code
Copy
class SuggestionsTableViewController: UITableViewController, HitsController, SearchBoxController { var onQueryChanged: ((String?) -> Void)? var onQuerySubmitted: ((String?) -> Void)? public var hitsSource: HitsInteractor<QuerySuggestion>? let cellID = "сellID" public override init(style: UITableView.Style) { super.init(style: style) tableView.register(SearchSuggestionTableViewCell.self, forCellReuseIdentifier: cellID) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setQuery(_ query: String?) { // not applicable } public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return hitsSource?.numberOfHits() ?? 0 } public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as? SearchSuggestionTableViewCell else { return .init() } if let suggestion = hitsSource?.hit(atIndex: indexPath.row) { cell.setup(with: suggestion) cell.didTapTypeAheadButton = { self.onQueryChanged?(suggestion.query) } } return cell } public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let suggestion = hitsSource?.hit(atIndex: indexPath.row) else { return } onQuerySubmitted?(suggestion.query) }}
The last step is the declaration of the QuerySuggestionsDemoViewController. It includes the business logic performing the multi-index search in the products and suggestions indices simultaneously.
First, declare, and initialize all the necessary components:
Swift
Report incorrect code
Copy
import Foundationimport UIKitimport InstantSearchpublic class QuerySuggestionsDemoViewController: UIViewController { let searchController: UISearchController let searcher: MultiSearcher let searchBoxConnector: SearchBoxConnector let textFieldController: TextFieldController let suggestionsHitsConnector: HitsConnector<QuerySuggestion> let suggestionsViewController: SuggestionsTableViewController let resultsHitsConnector: HitsConnector<Hit<StoreItem>> let resultsViewController: StoreItemsTableViewController override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { searcher = .init(appID: "latency", apiKey: "927c3fe76d4b52c5a2912973f35a3077") let suggestionsSearcher = searcher.addHitsSearcher(indexName: "STAGING_native_ecom_demo_products_query_suggestions") suggestionsViewController = .init(style: .plain) suggestionsHitsConnector = HitsConnector(searcher: suggestionsSearcher, interactor: .init(infiniteScrolling: .off), controller: suggestionsViewController) let resultsSearcher = searcher.addHitsSearcher(indexName: "STAGING_native_ecom_demo_products") resultsViewController = .init(style: .plain) resultsHitsConnector = HitsConnector(searcher: resultsSearcher, interactor: .init(), controller: resultsViewController) searchController = .init(searchResultsController: suggestionsViewController) textFieldController = .init(searchBar: searchController.searchBar) searchBoxConnector = .init(searcher: searcher, controller: textFieldController) super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }}
Now add a setup function to create the necessary connections between the components and set them up, establishing the business logic of your view controller. It must be called after the initialization of these components.
Swift
Report incorrect code
Copy
import Foundationimport UIKitimport InstantSearchpublic class QuerySuggestionsDemoViewController: UIViewController { let searchController: UISearchController let searcher: MultiSearcher let searchBoxConnector: SearchBoxConnector let textFieldController: TextFieldController let suggestionsHitsConnector: HitsConnector<QuerySuggestion> let suggestionsViewController: SuggestionsTableViewController let resultsHitsConnector: HitsConnector<Hit<StoreItem>> let resultsViewController: StoreItemsTableViewController override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { searcher = .init(appID: "latency", apiKey: "927c3fe76d4b52c5a2912973f35a3077") let suggestionsSearcher = searcher.addHitsSearcher(indexName: "STAGING_native_ecom_demo_products_query_suggestions") suggestionsViewController = .init(style: .plain) suggestionsHitsConnector = HitsConnector(searcher: suggestionsSearcher, interactor: .init(infiniteScrolling: .off), controller: suggestionsViewController) let resultsSearcher = searcher.addHitsSearcher(indexName: "STAGING_native_ecom_demo_products") resultsViewController = .init(style: .plain) resultsHitsConnector = HitsConnector(searcher: resultsSearcher, interactor: .init(), controller: resultsViewController) searchController = .init(searchResultsController: suggestionsViewController) textFieldController = .init(searchBar: searchController.searchBar) searchBoxConnector = .init(searcher: searcher, controller: textFieldController) super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) setup() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setup() { navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = false searchController.showsSearchResultsController = true addChild(resultsViewController) resultsViewController.didMove(toParent: self) searchBoxConnector.connectController(suggestionsViewController) searchBoxConnector.interactor.onQuerySubmitted.subscribe(with: searchController) { (searchController, _) in searchController.dismiss(animated: true, completion: .none) } searcher.search() }}
Finally, add sub-views to the view controller, and specify the Auto Layout constraints so that the display looks good on any device.Add the configureUI() function to your file and call it from viewDidLoad:
Swift
Report incorrect code
Copy
import Foundationimport UIKitimport InstantSearchpublic class QuerySuggestionsDemoViewController: UIViewController { let searchController: UISearchController let searcher: MultiSearcher let searchBoxConnector: SearchBoxConnector let textFieldController: TextFieldController let suggestionsHitsConnector: HitsConnector<QuerySuggestion> let suggestionsViewController: SuggestionsTableViewController let resultsHitsConnector: HitsConnector<Hit<StoreItem>> let resultsViewController: StoreItemsTableViewController override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { searcher = .init(appID: "latency", apiKey: "927c3fe76d4b52c5a2912973f35a3077") let suggestionsSearcher = searcher.addHitsSearcher(indexName: "STAGING_native_ecom_demo_products") suggestionsViewController = .init(style: .plain) suggestionsHitsConnector = HitsConnector(searcher: suggestionsSearcher, interactor: .init(infiniteScrolling: .off), controller: suggestionsViewController) let resultsSearcher = searcher.addHitsSearcher(indexName: "STAGING_native_ecom_demo_products_query_suggestions") resultsViewController = .init(style: .plain) resultsHitsConnector = HitsConnector(searcher: resultsSearcher, interactor: .init(), controller: resultsViewController) searchController = .init(searchResultsController: suggestionsViewController) textFieldController = .init(searchBar: searchController.searchBar) searchBoxConnector = .init(searcher: searcher, controller: textFieldController) super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) setup() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func viewDidLoad() { super.viewDidLoad() configureUI() } private func setup() { navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = false searchController.showsSearchResultsController = true addChild(resultsViewController) resultsViewController.didMove(toParent: self) searchBoxConnector.connectController(suggestionsViewController) searchBoxConnector.interactor.onQuerySubmitted.subscribe(with: searchController) { (searchController, _) in searchController.dismiss(animated: true, completion: .none) } searcher.search() } private func configureUI() { title = "Query Suggestions" view.backgroundColor = .white let resultsView = resultsViewController.view! resultsView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(resultsView) NSLayoutConstraint.activate([ resultsView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), resultsView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), resultsView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), resultsView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) }}
Your query suggestions search experience is now ready to use. Initialize your QuerySuggestionsDemoViewController and push it in your navigation controller hierarchy.
Swift
Report incorrect code
Copy
let searchViewController = QuerySuggestionsDemoViewController()navigationController?.pushViewController(searchViewController, animated: true)