Getting Started Programmatically
On this page
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.
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
. APodfile
will be created for you. - Open your
Podfile
and addpod '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 addimport 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")
Search your data: SearchBar
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.
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 theSearcher
.
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. AddconfigureUI
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
.
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 thesetup
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
.
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.