Concepts / Building Search UI / Getting started programmatically
Aug. 20, 2019

Getting Started Programmatically

Welcome to InstantSearch iOS

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 from scratch a full search interface!

We’ll show you how to build your search experience programmatically.

Before we start

To use InstantSearch iOS, you need an Algolia account. You can create one by clicking here, or use the following credentials:

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

These credentials will let you use 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

We will use CocoaPods for adding the dependency to InstantSearch.

  • If you don’t have CocoaPods installed on your machine, open your terminal and run sudo gem install cocoapods.
  • Go to the root of your project then type pod init. A Podfile will be created for you.
  • Open your Podfile and add pod 'InstantSearch', '~> 4.0' below your target.
  • On your terminal, run pod update.
  • Close your Xcode project, and then, at the root of your project, type open projectName.xcworkspace (replacing projectName with the actual name of your project).

Initialize your searcher

The main part of search experience is the Searcher. It performs search requests and obtains search results. Almost all InstantSearch components are connected with the Searcher. In this tutorial we deal with only one index, so let’s instantiate SingleIndexSearcher with proper credentials.

  • Go to your ViewController.swift file and then add import InstantSearch at the top.
  • Below your class definition, declare and instantiate your searcher.
1
2
3
let searcher = SingleIndexSearcher(appID: "latency",
                                   apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                   indexName: "bestbuy")

InstantSearch iOS provides you a set of single-responsability components allowing you to construct a flexible search experience perfectly satisfying your needs.
Let’s add the SearchBox to your application since any search experience requires one.

Guide searchbar

To create it you need declare QueryInputInteractor and SearchBarController.

  • SearchBarController updates the state and captures query text of UISearchBar.
  • QueryInputInteractor launches a new search each time the controller triggers it.
  • Declare and instantiate them in your ViewController class below the Searcher.
1
2
3
4
5
6
let searcher: SingleIndexSearcher = SingleIndexSearcher(appID: "latency",
                                                        apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                                        indexName: "bestbuy")

let queryInputInteractor: QueryInputInteractor = .init()
let searchBarController: SearchBarController = .init(searchBar: UISearchBar())

Now let’s add the Stats to show how the number of results changes when you type a query in your SearchBox. Similarly to the SearchBox, it consists of two components: StatsInteractor and LabelStatsController.

  • LabelStatsController updates the text of UILabel once new search stats information received.
  • StatsInteractor picks search stats information from search result and provides it to stats controller.
  • Declare and instantiate them bellow previous declarations:
1
2
3
4
5
6
7
8
9
10
  let searcher: SingleIndexSearcher = SingleIndexSearcher(appID: "latency",
                                                          apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                                          indexName: "bestbuy")

  let queryInputInteractor: QueryInputInteractor = .init()
  let searchBarController: SearchBarController = .init(searchBar: UISearchBar())

  let statsInteractor: StatsInteractor = .init()
  let statsController: LabelStatsController = .init(label: UILabel())
}
  • Declare setup() method connecting all the components together.
  • Then, inside your viewDidLoad method, make a call of this method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override func viewDidLoad() {
    super.viewDidLoad()
    setup()
}

func setup() {
    // Notify searcher about query text changes and launch a new search
    queryInputInteractor.connectSearcher(searcher)

    // Update query text in interactor each time query text changed in a UISearchBar
    queryInputInteractor.connectController(searchBarController)

    // Update search stats each time searcher receives new search results
    statsInteractor.connectSearcher(searcher)

    // Update label with up-to-date stats data
    statsInteractor.connectController(statsController)

    // Launch initial search
    searcher.search()
}
  • Finally, we need to add the views to the ViewController’s view and specify the autolayout constraints so that the layout looks good on any device. You don’t have to focus too much on understanding this part since it is not related to InstantSearch, and more related to iOS layout. Add configureUI function to your file and call it from viewDidLoad:
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
override func viewDidLoad() {
    super.viewDidLoad()
    setup()
    configureUI()
}

func configureUI() {
    
    view.backgroundColor = .white
    
    // Declare a stack view containing all the components
    let stackView = UIStackView()
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.spacing = 16
    stackView.axis = .vertical
    stackView.layoutMargins = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 0)
    stackView.isLayoutMarginsRelativeArrangement = true
    
    // Add searchBar
    let searchBar = searchBarController.searchBar
    searchBar.translatesAutoresizingMaskIntoConstraints = false
    searchBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
    searchBar.searchBarStyle = .minimal
    stackView.addArrangedSubview(searchBar)
    
    // Add statsLabel
    let statsLabel = statsController.label
    statsLabel.translatesAutoresizingMaskIntoConstraints = false
    statsLabel.heightAnchor.constraint(equalToConstant: 16).isActive = true
    stackView.addArrangedSubview(statsLabel)

    stackView.addArrangedSubview(UIView())

    // Pin stackView to ViewController's view
    view.addSubview(stackView)

    NSLayoutConstraint.activate([
      stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
      stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
      stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
    ])

}

Build and run your application: you now have the most basic search experience! You should see that the results are changing on each key stroke. Fantastic!

Recap

You just used your very first components from InstantSearch. In this part, you’ve learned:

  • How to create a Searcher
  • How to create a SearchBox
  • How to create a Stats
  • How to connect all these components together

Display your data: Hits

The whole point of a search experience is to display the dataset that best matches the query entered by the user. That’s what we will implement in this section with the Hits.

Guide hits

Hits also consists of two components: HitsInteractor and HitsTableController.

  • HitsTableController updates the content of UITableView once new search result received.
  • HitsInteractor picks search hits from search result and provides them to hits controller.
  • Declare and instantiate them bellow previous declarations as follows:
1
2
3
4
5
6
7
8
9
10
11
12
let searcher: SingleIndexSearcher = SingleIndexSearcher(appID: "latency",
                                                        apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                                        indexName: "bestbuy")

let queryInputInteractor: QueryInputInteractor = .init()
let searchBarController: SearchBarController = .init(searchBar: UISearchBar())

let statsInteractor: StatsInteractor = .init()
let statsController: LabelStatsController = .init(label: UILabel())

let hitsInteractor: HitsInteractor<JSON> = .init()
let hitsTableController: HitsTableController<HitsInteractor<JSON>> = .init(tableView: UITableView())
  • Add connections between the Hits components with each other and with searcher in the setup method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func setup() {
    // Notify searcher about query text changes and launch a new search
    queryInputInteractor.connectSearcher(searcher)

    // Update query text in interactor each time query text changed in a UISearchBar
    queryInputInteractor.connectController(searchBarController)

    // Update search stats each time searcher receives new search results
    statsInteractor.connectSearcher(searcher)

    // Update label with up-to-date stats data
    statsInteractor.connectController(statsController)

    // Update hitsInteractor with up-to-date search results
    hitsInteractor.connectSearcher(searcher)

    // Dispatch search results to tableView
    hitsInteractor.connectController(hitsTableController)

    // Launch initial search
    searcher.search()
}

Now that we have our tableView setup, we still need to specify which fields from the Algolia response we want to show:

  • Register UITableViewCell in tableView in classical way
  • Use the hitsTableController dataSource instance providing a closure-based way for dequeueing your cell and setting it up with a search result hit.
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
func setup() {
    // Notify searcher about query text changes and launch a new search
    queryInputInteractor.connectSearcher(searcher)

    // Update query text in interactor each time query text changed in a UISearchBar
    queryInputInteractor.connectController(searchBarController)

    // Update search stats each time searcher receives new search results
    statsInteractor.connectSearcher(searcher)

    // Update label with up-to-date stats data
    statsInteractor.connectController(statsController)

    // Update hitsInteractor with up-to-date search results
    hitsInteractor.connectSearcher(searcher)

    // Dispatch search results to tableView
    hitsInteractor.connectController(hitsTableController)

    // Register cell in tableView
    hitsTableController.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID")
    
    // Dequeue and setup cell with hit data
    hitsTableController.dataSource = .init() { tableView, hit, indexPath in
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath)
        cell.textLabel?.text = [String: Any](hit)?["name"] as? String
        return cell
    }

    // Launch initial search
    searcher.search()
}

Here we use the json hit, extract the name of the product, and assign it to the text property of the cell’s textLabel.

  • Then we will add the table to a stackView of our ViewController.
  • Replace your configureUI method with the following (again, no need to worry about understanding this part):
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
func configureUI() {

    view.backgroundColor = .white

    // Declare a stack view containing all the components
    let stackView = UIStackView()
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.spacing = 16
    stackView.axis = .vertical
    stackView.layoutMargins = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 0)
    stackView.isLayoutMarginsRelativeArrangement = true
    
    // Add searchBar
    let searchBar = searchBarController.searchBar
    searchBar.translatesAutoresizingMaskIntoConstraints = false
    searchBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
    searchBar.searchBarStyle = .minimal
    stackView.addArrangedSubview(searchBar)
    
    // Add statsLabel
    let statsLabel = statsController.label
    statsLabel.translatesAutoresizingMaskIntoConstraints = false
    statsLabel.heightAnchor.constraint(equalToConstant: 16).isActive = true
    stackView.addArrangedSubview(statsLabel)
    
    // Add hits tableView
    stackView.addArrangedSubview(hitsTableController.tableView)

    // Pin stackView to ViewController's view
    view.addSubview(stackView)

    NSLayoutConstraint.activate([
      stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
      stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
      stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
    ])

}

Build and run your application: you now have an InstantSearch iOS app displaying your data! You can also enjoy the infinite scrolling of the table as well if you set it to true!

In this part, you’ve learned:

  • How to build your interface with InstantSearch components by adding the Hits.

Filter your results: RefinementList

Now you have an access to more than 10000 products which is great. But almost certainly you don’t want to scroll all of them in order to find the one you need. There must be a way to obtain more accurate results. Let’s add a possibility to filter our results using RefinementList components. You can read more about this set of components here. Let’s filter products by category.

First of all you should add a FilterState, a component for a convenient search filters management. Then let’s add a refinement attribute which is category in our case. Finally we have to add a components of RefinementList to our search experience such as SelectableFacetInteractor, FacetListTableController and UITableViewController which will actually present a facet list. As result, properties definitions of your ViewController must look as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let searcher: SingleIndexSearcher = .init(appID: "latency",
                                          apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                          indexName: "bestbuy")

let queryInputInteractor: QueryInputInteractor = .init()
let searchBarController: SearchBarController = .init(searchBar: UISearchBar())

let statsInteractor: StatsInteractor = .init()
let statsController: LabelStatsController = .init(label: UILabel())

let hitsInteractor: HitsInteractor<JSON> = .init()
let hitsTableController: HitsTableController<HitsInteractor<JSON>> = .init(tableView: UITableView())

let categoryAttribute: Attribute = "category"
let filterState: FilterState = .init()

let categoryInteractor: FacetListInteractor = .init(selectionMode: .single)
let categoryTableViewController: UITableViewController = .init()
lazy var categoryListController: FacetListTableController = {
  return .init(tableView: categoryTableViewController.tableView, titleDescriptor: .none)
}()

To make it work all together the new components must be properly connected. Searcher must be connected with FilterState in order to modify search query and relaunch search once filters changed. HitsInteractor must be connected as well with FilterState in order to be announced when query changed and it must invalidate cached hits.
Finally, let’s connect RefinementList components with each other and with FilterState through which it will communicate with a Searcher. Update your setup function as follows:

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
func setup() {
  searcher.connectFilterState(filterState)
  
  queryInputInteractor.connectSearcher(searcher)
  queryInputInteractor.connectController(searchBarController)
  
  statsInteractor.connectSearcher(searcher)
  statsInteractor.connectController(statsController)
  
  hitsInteractor.connectSearcher(searcher)
  hitsInteractor.connectController(hitsTableController)
  hitsInteractor.connectFilterState(filterState)

  hitsTableController.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID")
  hitsTableController.dataSource = .init(cellConfigurator: { tableView, hit, indexPath in
    let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath)
    cell.textLabel?.text = [String: Any](hit)?["name"] as? String
    return cell
  })
  
  categoryInteractor.connectSearcher(searcher, with: categoryAttribute)
  categoryInteractor.connectFilterState(filterState, with: categoryAttribute, operator: .and)
  categoryInteractor.connectController(categoryListController, with: FacetListPresenter(sortBy: [.isRefined]))
  
  searcher.search()
}

Replace your configureUI method with the following (again, no need to worry about understanding this part):

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
func configureUI() {

  view.backgroundColor = .white
  
  let stackView = UIStackView()
  stackView.translatesAutoresizingMaskIntoConstraints = false
  stackView.spacing = 16
  stackView.axis = .vertical
  stackView.layoutMargins = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 0)
  stackView.isLayoutMarginsRelativeArrangement = true
  
  let searchBar = searchBarController.searchBar
  searchBar.translatesAutoresizingMaskIntoConstraints = false
  searchBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
  searchBar.searchBarStyle = .minimal

  let filterButton = UIButton()
  filterButton.setTitleColor(.black, for: .normal)
  filterButton.setTitle("Filter", for: .normal)
  filterButton.addTarget(self, action: #selector(showFilters), for: .touchUpInside)
  
  let searchBarFilterButtonStackView = UIStackView()
  searchBarFilterButtonStackView.translatesAutoresizingMaskIntoConstraints = false
  searchBarFilterButtonStackView.spacing = 4
  searchBarFilterButtonStackView.axis = .horizontal
  searchBarFilterButtonStackView.addArrangedSubview(searchBar)
  searchBarFilterButtonStackView.addArrangedSubview(filterButton)
  let spacer = UIView()
  spacer.widthAnchor.constraint(equalToConstant: 4).isActive = true
  searchBarFilterButtonStackView.addArrangedSubview(spacer)
  
  stackView.addArrangedSubview(searchBarFilterButtonStackView)
  
  let statsLabel = statsController.label
  statsLabel.translatesAutoresizingMaskIntoConstraints = false
  statsLabel.heightAnchor.constraint(equalToConstant: 16).isActive = true
  stackView.addArrangedSubview(statsLabel)

  stackView.addArrangedSubview(hitsTableController.tableView)
  
  view.addSubview(stackView)
  
  NSLayoutConstraint.activate([
    stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
    stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
    ])
  
  categoryTableViewController.title = "Category"
  categoryTableViewController.view.backgroundColor = .white
  
}

Finally, add these functions in the end of you ViewController. They are responsible for presenting/dismissing your RefinementList.

1
2
3
4
5
6
7
8
9
  @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() {
    dismiss(animated: true, completion: .none)
  }

In the end your ViewController must look as follows:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import UIKit
import InstantSearch

class ViewController: UIViewController {
  
  let searcher: SingleIndexSearcher = .init(appID: "latency",
                                            apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
                                            indexName: "bestbuy")
  
  let queryInputInteractor: QueryInputInteractor = .init()
  let searchBarController: SearchBarController = .init(searchBar: UISearchBar())
  
  let statsInteractor: StatsInteractor = .init()
  let statsController: LabelStatsController = .init(label: UILabel())
  
  let hitsInteractor: HitsInteractor<JSON> = .init()
  let hitsTableController: HitsTableController<HitsInteractor<JSON>> = .init(tableView: UITableView())
  
  let categoryAttribute: Attribute = "category"
  let filterState: FilterState = .init()
  
  let categoryInteractor: FacetListInteractor = .init(selectionMode: .single)
  let categoryTableViewController: UITableViewController = .init()
  lazy var categoryListController: FacetListTableController = {
    return .init(tableView: categoryTableViewController.tableView, titleDescriptor: .none)
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setup()
    configureUI()
    navigationController?.setNavigationBarHidden(true, animated: false)
  }
    
  func setup() {
    
    searcher.connectFilterState(filterState)
    
    queryInputInteractor.connectSearcher(searcher)
    queryInputInteractor.connectController(searchBarController)
    
    statsInteractor.connectSearcher(searcher)
    statsInteractor.connectController(statsController)
    
    hitsInteractor.connectSearcher(searcher)
    hitsInteractor.connectController(hitsTableController)
    hitsInteractor.connectFilterState(filterState)

    hitsTableController.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID")
    hitsTableController.dataSource = .init(cellConfigurator: { tableView, hit, indexPath in
      let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath)
      cell.textLabel?.text = [String: Any](hit)?["name"] as? String
      return cell
    })
    
    categoryInteractor.connectSearcher(searcher, with: categoryAttribute)
    categoryInteractor.connectFilterState(filterState, with: categoryAttribute, operator: .and)
    categoryInteractor.connectController(categoryListController, with: FacetListPresenter(sortBy: [.isRefined]))
    
    searcher.search()
  }
  
  func configureUI() {
  
    view.backgroundColor = .white
    
    let stackView = UIStackView()
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.spacing = 16
    stackView.axis = .vertical
    stackView.layoutMargins = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 0)
    stackView.isLayoutMarginsRelativeArrangement = true
    
    let searchBar = searchBarController.searchBar
    searchBar.translatesAutoresizingMaskIntoConstraints = false
    searchBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
    searchBar.searchBarStyle = .minimal
    
    let filterButton = UIButton()
    filterButton.setTitleColor(.black, for: .normal)
    filterButton.setTitle("Filter", for: .normal)
    filterButton.addTarget(self, action: #selector(showFilters), for: .touchUpInside)
    
    let searchBarFilterButtonStackView = UIStackView()
    searchBarFilterButtonStackView.translatesAutoresizingMaskIntoConstraints = false
    searchBarFilterButtonStackView.spacing = 4
    searchBarFilterButtonStackView.axis = .horizontal
    searchBarFilterButtonStackView.addArrangedSubview(searchBar)
    searchBarFilterButtonStackView.addArrangedSubview(filterButton)
    let spacer = UIView()
    spacer.widthAnchor.constraint(equalToConstant: 4).isActive = true
    searchBarFilterButtonStackView.addArrangedSubview(spacer)
    
    stackView.addArrangedSubview(searchBarFilterButtonStackView)
    
    let statsLabel = statsController.label
    statsLabel.translatesAutoresizingMaskIntoConstraints = false
    statsLabel.heightAnchor.constraint(equalToConstant: 16).isActive = true
    stackView.addArrangedSubview(statsLabel)

    stackView.addArrangedSubview(hitsTableController.tableView)
    
    view.addSubview(stackView)
    
    NSLayoutConstraint.activate([
      stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
      stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
      stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
      ])
    
    categoryTableViewController.title = "Category"
    categoryTableViewController.view.backgroundColor = .white
    
  }
  
  @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() {
    dismiss(animated: true, completion: .none)
  }
  
}

Build and run your application: you now have a basic search results with filtering using RefinementList.

Guide refinements1

Guide refinements2

Go further

Your application now displays your data, lets your users enter a query, displays search results as-they-type and provides a possibility to filter them 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][components] to see other components that you could use.

Did you find this page helpful?

iOS InstantSearch v5