Guides / Building Search UI / Getting started

Getting Started Programmatically

In this guide, we will walk through the few steps needed to start a project with InstantSearch iOS. We will start from an empty iOS project, and create a full search experience from scratch!

This search experience will include:

  • A list to display search results
  • A searchbox to type your query
  • Statistics about the current search
  • A facet list for filtering results

Installation

To use InstantSearch iOS, you need an Algolia account. You can create a new account, or use the following credentials:

  • APP ID: latency
  • Search API Key: 1f6fd3a6fb973cb08419fe7d288fa4db
  • Index name: bestbuy

These credentials give access to a preloaded dataset of products appropriate for this guide.

Create a new project

Let’s get started! In Xcode, create a new Project:

  • On the Template screen, select Single View Application and click next
  • Specify your Product name, select Swift as the language and iPhone as the Device, and then create.

Xcode newproject

Prepare your storyboard

After generation, the project directly shows the Main View Controller. We’ll have to embed this into the Navigation Controller.

  • Open Main.storyboard
  • Select ViewController in the View Controller scene
  • Editor > Embed in > Navigation Controller

Xcode embed in navigation controller

Add InstantSearch dependency

To add the InstantSearch package dependency to your Xcode project, you need a dependency manager. This can be the Swift Package Manager, or CocoaPods.

Swift Package Manager

  • Select File > Swift Packages > Add Package Dependency and enter this repository URL: https://github.com/algolia/instantsearch-ios
  • You can also navigate to your target’s General pane, and in the Frameworks, Libraries, and Embedded Content section, click the + button, select Add Other, and choose Add Package Dependency.
  • In the package products selection dialog, select both the InstantSearch and InstantSearchCore dependencies.

CocoaPods

  • If you don’t have CocoaPods installed on your machine, open your terminal and run sudo gem install cocoapods.
  • In your terminal, navigate to the root directory of your project and run the pod init command. This command generates a Podfile for you.
  • Open your Podfile and add pod 'InstantSearch', '~> 7' below your target.
  • In your terminal, run pod update.
  • Close your Xcode project. In your terminal, at the root of your project, execute the open projectName.xcworkspace command (replacing projectName with the actual name of your project).

Implementation

Xcode automatically generates a ViewController.swift file when you create a Single View Application. Open this file, and add the following import statement at the top.

1
import InstantSearch

Define your record structure

We’ll have to define a structure that represent a record in your index. For simplicity’s sake, our structure only provides the name of the product. Your structure must conform to the Codable protocol to work properly with InstantSearch. Add the following structure definition to your ViewController.swift file:

1
2
3
struct BestBuyItem: Codable {
  let name: String
}

Declare Hits View Controller

In this tutorial we’ll use HitsTableViewController, a basic implementation of the HitsController protocol. It’s a generic view controller, parameterized with an implementation of the TableViewCellConfigurable protocol. This implementation defines how to bind the record’s data to the UITableViewCell instance. The following example binds the name of the fetched item to the cell’s textLabels text property.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct BestBuyTableViewCellConfigurator: TableViewCellConfigurable {
   
  let model: BestBuyItem
  
  init(model: BestBuyItem, indexPath: IndexPath) {
    self.model = model
  }
  
  func configure(_ cell: UITableViewCell) {
    cell.textLabel?.text = model.name
  }

}

Then, we’ll define a convenient typealias for the HitsTableViewController parameterized with CellConfigurator.

1
typealias BestBuyHitsViewController = HitsTableViewController<BestBuyTableViewCellConfigurator>

Refer to our HitsController documentation for more on integrating and customizing the `Hits’ widget.

Complete the View Controller

To complete the main view controller of your application, we’ll need to declare a UISearchController. This is a UIKit component that manages the display of search results based on interactions with a search bar. It provides a search bar, and only requires a search results controller as a parameter. Let’s add a hitsViewController field to our view controller, with the type we declared in the previous step. We have to set this as an initializer parameter of the search controller.

1
2
3
4
5
6
7
8
9
10
class ViewController: UIViewController {
  
  lazy var searchController: UISearchController = .init(searchResultsController: hitsViewController)
  let hitsViewController: BestBuyHitsViewController = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }

}

Initialize your searcher

With the necessary view controllers in place, it’s time to add our search logic. The central part of our search experience is the Searcher. The Searcher performs search requests and obtains search results. Almost all InstantSearch components are connected with the Searcher. Since we’re only targeting one index, we will instantiate a SingleIndexSearcher as our searcher using the proper credentials.

Then, we’ll add a searchConnector property to the view controller. The purpose of Connectors in InstantSearch is to establish links between its components. We’ll Initialize the SearchConnector with the Searcher, search controller and hits view controller as parameters. Finally, activate the search connector by calling its connect() method and then add searcher.search() to launch the first empty search request in the viewDidLoad method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ViewController: UIViewController {
      
  let searcher = SingleIndexSearcher(appID: "latency",
                                     apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                     indexName: "bestbuy")
  lazy var searchController: UISearchController = .init(searchResultsController: hitsViewController)
  lazy var searchConnector: SingleIndexSearchConnector<BestBuyItem> = .init(searcher: searcher,
                                                                            searchController: searchController,
                                                                            hitsController: hitsViewController)
  let hitsViewController: BestBuyHitsViewController = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    searchConnector.connect()
    searcher.search()
  }
            
}

While fully functional, we can’t use our search logic yet because we don’t display our search. We need to add a setupUI method to the view controller and call it from the viewDidLoad method. Finally, we override the viewDidAppear method, and set our search bar as the first responder so that the search controller presents results immediately after the view controller appearance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ViewController: UIViewController {
      
  let searcher = SingleIndexSearcher(appID: "latency",
                                     apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                     indexName: "bestbuy")
  lazy var searchController: UISearchController = .init(searchResultsController: hitsViewController)
  lazy var searchConnector: SingleIndexSearchConnector<BestBuyItem> = .init(searcher: searcher,
                                                                            searchController: searchController,
                                                                            hitsController: hitsViewController)
  let hitsViewController: BestBuyHitsViewController = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    searchConnector.connect()
    searcher.search()
    setupUI()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.searchBar.becomeFirstResponder()
  }
  
  func setupUI() {
    view.backgroundColor = .white
    navigationItem.searchController = searchController
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.showsSearchResultsController = true
    searchController.automaticallyShowsCancelButton = false
  }
      
}

That’s all! You can now build and run your application to see the basic search experience in action. You should see that the results are changing on each key stroke.

Guide hits

Adding statistics

To make our search experience more user-friendly, we can provide additional feedback about the search results. We’ll explore how to extend our search experience with different InstantSearch modules. First, we’ll add a statistics component. This component will show the hit count. This helps give the user a complete understanding about their search, without the need for extra interaction. We’ll also create a StatsInteractor, which extracts the metadata that we need from the search response, and provides an interface to present it to the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ViewController: UIViewController {
      
  let searcher = SingleIndexSearcher(appID: "latency",
                                     apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                     indexName: "bestbuy")
  lazy var searchController: UISearchController = .init(searchResultsController: hitsViewController)
  lazy var searchConnector: SingleIndexSearchConnector<BestBuyItem> = .init(searcher: searcher,
                                                                            searchController: searchController,
                                                                            hitsController: hitsViewController)
  let hitsViewController: BestBuyHitsViewController = .init()
  let statsInteractor: StatsInteractor = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    searchConnector.connect()
    statsInteractor.connectSearcher(searcher)
    searcher.search()
    setupUI()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.searchBar.becomeFirstResponder()
  }
  
  func setupUI() {
    view.backgroundColor = .white
    navigationItem.searchController = searchController
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.showsSearchResultsController = true
    searchController.automaticallyShowsCancelButton = false
  }
      
}

Our StatsInteractor receives our search statistics now, but we don’t display them anywhere yet. To keep our example simple, we’ll present the hits count as the title of our view controller. This is probably not the best place to show this in your interface, but this prevents putting to much layout-related code in this guide. We’ll let our StatsInteractor present its data in a component that implements the StatsTextController protocol. You have to make your view controller conform to this protocol by adding an extension. Now, you can connect the view controller the StatsInteractor with the corresponding method. Add this connection in the viewDidLoad method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class ViewController: UIViewController {
      
  let searcher = SingleIndexSearcher(appID: "latency",
                                     apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                     indexName: "bestbuy")
  lazy var searchController: UISearchController = .init(searchResultsController: hitsTableViewController)
  lazy var searchConnector: SingleIndexSearchConnector<BestBuyItem> = .init(searcher: searcher,
                                                                            searchController: searchController,
                                                                            hitsController: hitsTableViewController)
  let hitsTableViewController: BestBuyHitsViewController = .init()
  let statsInteractor: StatsInteractor = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    searchConnector.connect()
    statsInteractor.connectSearcher(searcher)
    statsInteractor.connectController(self)
    searcher.search()
    setupUI()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.searchBar.becomeFirstResponder()
  }
  
  func setupUI() {
    view.backgroundColor = .white
    navigationItem.searchController = searchController
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.showsSearchResultsController = true
    searchController.automaticallyShowsCancelButton = false
  }
      
}

extension ViewController: StatsTextController {
  
  func setItem(_ item: String?) {
    title = item
  }

}

Build and run your application: you should now see updated results and an updated hit count on each keystroke.

Guide hits count

We now have a better understanding of the organization of InstantSearch modules:

  • Each module has an Interactor, containing the module’s business-logic.
  • Each Interactor has a corresponding Controller protocol, which defines the interaction with a UI component.

Out of the box, InstantSearch provides a few basic implementations of the Controller protocol for UIKit components. Examples of these are HitsTableViewController (which we use in this tutorial), TextFieldController, and ActivityIndicatorController. Feel free to use them to discover the abilities of InstantSearch with minimal effort. In your own project, you might want implement more custom UI and behavior. If so, it’s up to you to create an implementations of the Controller protocol, and to connect them to a corresponding Interactor.

Filter your results: RefinementList

With your app, you can search more than 10,000 products. However, you don’t want to scroll to the bottom of the list to find the exact product you’re looking for. We can more accurately filter our results by making use of the RefinementList components. We’ll build a filter that allows us to filter products by their category. First, add a FilterState component. This component provides a convenient way to manage the state of your filters. In our case, we’ll add a refinement attribute: category. Finally, we have to add the RefinementList components to other components in our search experience, such as FacetListConnector, FacetListTableController and UITableViewController. The UITableViewController will actually present a facet list. As a result, the definition of your ViewController has to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class ViewController: UIViewController {
      
  let searcher = SingleIndexSearcher(appID: "latency",
                                     apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                     indexName: "bestbuy")
  lazy var searchController: UISearchController = .init(searchResultsController: hitsViewController)
  lazy var searchConnector: SingleIndexSearchConnector<BestBuyItem> = .init(searcher: searcher,
                                                                            searchController: searchController,
                                                                            hitsController: hitsViewController,
                                                                            filterState: filterState)
  let hitsViewController: BestBuyHitsViewController = .init()
  let statsInteractor: StatsInteractor = .init()
  let filterState: FilterState = .init()
  lazy var categoryConnector: FacetListConnector = .init(searcher: searcher,
                                                         filterState: filterState,
                                                         attribute: "category",
                                                         operator: .or,
                                                         controller: categoryListController,
                                                         presenter: FacetListPresenter(sortBy: [.isRefined]))
  
  lazy var categoryListController: FacetListTableController = .init(tableView: categoryTableViewController.tableView)
  let categoryTableViewController: UITableViewController = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    searchConnector.connect()
    categoryConnector.connect()
    statsInteractor.connectSearcher(searcher)
    statsInteractor.connectController(self)
    searcher.search()
    setupUI()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.searchBar.becomeFirstResponder()
  }
  
  func setupUI() {
    view.backgroundColor = .white
    navigationItem.searchController = searchController
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.showsSearchResultsController = true
    searchController.automaticallyShowsCancelButton = false
  }
      
}

extension ViewController: StatsTextController {
  
  func setItem(_ item: String?) {
    title = item
  }
  
}

Finally, in the setupUI() method, set up a navigation bar button item that triggers the presentation of our facet list , and sets the title of this list. We’ll also add showFilters and dismissFilters functions responsible for the presentation and dismiss logic of the facet list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class ViewController: UIViewController {
      
  let searcher = SingleIndexSearcher(appID: "latency",
                                     apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                     indexName: "bestbuy")
  lazy var searchController: UISearchController = .init(searchResultsController: hitsViewController)
  lazy var searchConnector: SingleIndexSearchConnector<BestBuyItem> = .init(searcher: searcher,
                                                                            searchController: searchController,
                                                                            hitsController: hitsViewController,
                                                                            filterState: filterState)
  let hitsViewController: BestBuyHitsViewController = .init()
  let statsInteractor: StatsInteractor = .init()
  let filterState: FilterState = .init()
  lazy var categoryConnector: FacetListConnector = .init(searcher: searcher,
                                                         filterState: filterState,
                                                         attribute: "category",
                                                         operator: .or,
                                                         controller: categoryListController,
                                                         presenter: FacetListPresenter(sortBy: [.isRefined]))
  
  lazy var categoryListController: FacetListTableController = .init(tableView: categoryTableViewController.tableView)
  let categoryTableViewController: UITableViewController = .init()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    searchConnector.connect()
    categoryConnector.connect()
    statsInteractor.connectSearcher(searcher)
    statsInteractor.connectController(self)
    searcher.search()
    setupUI()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.searchBar.becomeFirstResponder()
  }
  
  func setupUI() {
    view.backgroundColor = .white
    navigationItem.searchController = searchController
    navigationItem.rightBarButtonItem = .init(title: "Category", style: .plain, target: self, action: #selector(showFilters))
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.showsSearchResultsController = true
    searchController.automaticallyShowsCancelButton = false
    categoryTableViewController.title = "Category"
  }
  
  @objc func showFilters() {
    let navigationController = UINavigationController(rootViewController: categoryTableViewController)
    categoryTableViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissFilters))
    present(navigationController, animated: true, completion: .none)
  }
  
  @objc func dismissFilters() {
    categoryTableViewController.navigationController?.dismiss(animated: true, completion: .none)
  }
  
}

extension ViewController: StatsTextController {
  
  func setItem(_ item: String?) {
    title = item
  }
  
}

You can now build and run your application to see your RefinementList in action.

Guide refinements1

Guide refinements2

Going further

Your users can enter a query, and your application shows them results as they type. It also provides a possibility to filter the results even further using RefinementList. That is pretty nice already! However, we can go further and improve on that.

  • You can have a look at our examples to see more complex examples of applications built with InstantSearch.
  • You can head to our components page to see other components that you could use.

Did you find this page helpful?