Concepts / Building Search UI / Storyboard
Jan. 27, 2019

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

We will use storyboards (or xibs). If you prefer to write your UI programmatically, you can follow the programmatic getting started guide.

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_promo

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. A Podfile will be created for you.
  • Open your Podfile and add pod 'InstantSearch' 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).

For more ways to add InstantSearch to your project, please refer to our Installing InstantSearch guide.

Initialize InstantSearch

To initialize InstantSearch, you need an Algolia account with a configured non-empty index.

Go to your AppDelegate.swift file and then add import InstantSearch at the top. Then inside your application(_:didFinishLaunchingWithOptions:) method, add the following:

1
2
3
4
5
// AppDelegate.swift inside application(_:didFinishLaunchingWithOptions:)

InstantSearch.shared.configure(appID: "latency", apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db", index: "bestbuy_promo")
InstantSearch.shared.params.attributesToRetrieve = ["name", "salePrice"]
InstantSearch.shared.params.attributesToHighlight = ["name"]

This will initialize InstantSearch with the credentials proposed at the beginning. You can also chose to replace them with the credentials of your own app.

To understand the above, we are using the singleton InstantSearch.shared to configure InstantSearch with our Algolia credentials. InstantSearch.shared will be used throughout our app to easily manage InstantSearch. You could also have created your own instance of InstantSearch and passed it around your controllers, but we won’t do that in this guide.

Next, we added the attributes that we want to retrieve and highlight. As a side note, some search parameters can be defaulted in the Algolia dashboard by going to Indices -> Display tab. If you add the configuration there, then you do not need to specify the attributesToRetrieve and attributesToHighlight as shown above.

InstantSearch iOS is based on a system of widgets that constitute your search interface. The first widget we’ll add is a SearchBar since any search experience requires one. InstantSearch will automatically recognize your SearchBar as a source of search queries. We will also add a Stats widget to show how the number of results change when you type a query in your SearchBar. This is how it will look like.

  • Start by going to your ViewController.swift file and then add import InstantSearch at the top.
  • Then inside your viewDidLoad method, add the following:
1
2
3
4
// ViewController.swift inside viewDidLoad

// Register all widgets in view to InstantSearch
InstantSearch.shared.registerAllWidgets(in: self.view)

Here, we’re telling InstantSearch to inspect all the subviews in the ViewController’s view. So what we need to do now is add the widgets to our view!

  • Open your Main.storyboard file, and on the Utility Tab on your right, go to the Object Library at the bottom.
  • Drag and drop a Search Bar to your view. Click on it and then on the identity inspector, add the custom class SearchBarWidget.
  • Now repeat the process but this time add a Label to the view, and then let the custom class be a StatsLabelWidget.
  • Finally, make the width of the label bigger so that the text can clearly appear.

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 widgets from InstantSearch. In this part, you’ve learned:

  • How to create a SearchBar Widget
  • How to create a StatsLabel Widget
  • How to register widgets to InstantSearch

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 widget.

  • In your Main.Storyboard, drag and drop a Table View from the Object Library and resize it to make it bigger.
  • Select the Table View and change its custom class to HitsTableWidget.

If you go to the attributes inspector, you will see that at the top, there are 4 configuration parameters that you can change, like Hits Pet Page and Infinite Scrolling. Feel free to change them to your needs, or keep the default values.

  • Click on your Table View, and in the attributes inspector, add a prototype cell under the Table View section by replacing 0 with 1.
  • You should now be able to see a Table View Cell (under your Table View) in the Document Outline on the left of the storyboard file. Select that, and in the attributes inspector, specify hitCell as the identifier.
  • Finally, we need to have a reference to the HitsTableView in your ViewController. For that, go ahead and create an IBOutlet and call it tableView.

Now that we have our Table View setup, we still need to specify what fields from the Algolia response we want to show, as well as the layout of our cells. InstantSearch provides both base classes and helper classes in order to achieve this. Here, we will look at the easiest and most flexible way: using the base class HitsTableViewController.

  • In your ViewController class, replace UIViewController with HitsTableViewController. This class will help you setup a lot of boilerplate code for you.
1
class ViewController: HitsTableViewController
  • Next, in your viewDidLoad method, before registering your widgets to InstantSearch, add the following:
1
2
3
// ViewController.swift in viewDidLoad

hitsTableView = tableView

This will associate the hitsTableView in the base class to the tableView that we just created. Behind the scenes, your ViewController class will become the delegate and dataSource of the tableView, the same way the UIKit base class UITableViewController does that for you.

Next, we can specify our cells with a method provided by the base class, which contains the hit for the specific row.

1
2
3
4
5
6
7
8
9
// ViewController.swift

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing hit: [String : Any]) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "hitCell", for: indexPath)

    cell.textLabel?.text = hit["name"] as? String

    return cell
}

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

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 if you set it to true!

In this part, you’ve learned:

  • How to build your interface with widgets by adding the Hits widget
  • How to configure widgets
  • How to specify the look and feel of your hit cells

Your application now displays your data, lets your users enter a query, and displays search results as-they-type. That is pretty nice already! However, we can go further and improve on that.

Highlight your results

Your current application lets the user search and displays results, but doesn’t explain why these results match the user’s query.

You can improve it by using the Highlighting feature: InstantSearch offers a helper method just for that.

  • At the top of your ViewController.swift file, add import InstantSearch.
  • In your tableView(_:cellForRowAt:) method, remove the statement cell.textLabel?.text = hit["name"] as? String as we don’t need it anymore, and add the following before the return cell statement:
1
2
3
4
5
// ViewController.swift in tableView(_:cellForRowAt:)

cell.textLabel?.highlightedTextColor = .blue
cell.textLabel?.highlightedBackgroundColor = .yellow
cell.textLabel?.highlightedText = SearchResults.highlightResult(hit: hit, path: "name")?.value

Restart your application and type something in the SearchBar: the results are displayed with your keywords highlighted in these views!

In this part, you’ve learned:

  • Highlighting search results.

Filter your results: RefinementList

A refinementList lets the user refine the search results by choosing filters from a list of filters. In this section, we will build a refinementList to filter results based on the categories selected. We will only go through the storyboard approach here.

Setup New Screen and Navigation

  • Go to File -> New -> File, then select Cocoa Touch Class, and name it RefinementViewController, then create it.
  • Let’s first setup the navigation controller. In your Main.storyboard file, click on your ViewController, then in your menu bar at the top, go to Editor -> Embed in -> Navigation Controller.
  • In your Utilities bar on your right, drag and drop a View Controller from the Object library to your storyboard. Click on it, and then in the Identity Inspector, change its custom class to RefinementViewController.
  • In your Object library again, drag and drop a Bar Button Item to the Navigation bar of the first ViewController. Double click on it to change its name to “Filter”.
  • From this button, hold on Ctrl, and drag it to the RefinementViewConroller view, and select Show as the Action Segue.

Now that we have our navigation setup, let’s add our refinementList as a tableView.

  • Drag and drop a Table View from the Object Library onto the RefinementViewController view, and change its custom class to be RefinementTableWidget. Go ahead and create a prototype cell for this table and specify refinementCell as the identifier. Also, change the Style of the cell in the identity inspector to Subtitle.
  • Then, create an IBOutlet of that tableView into RefinementViewController.swift, and call it tableView.
  • Finally, add the import InstantSearch statement at the top of RefinementViewController.swift.

The RefinementList

We can implement a RefinementList with the exact same idea as the Hits widgets: using a base class and then implementing some delegate methods. However, this time, we will implement it using the helper class in order to show you how things can be done differently. That will help you use InstantSearch in the case where your ViewController already inherits from a subclass of UIViewController, and not UIViewController itself. Additionally, since you cannot subclass a Swift class in Objective-C, this method will be useful if you decide to write your app in Objective-C.

  • First things first. Go to Main.Storyboard and select the tableView in the screen containing your refinementList. This will be your refinementList. Note that we already changed the class of the table to be a RefinementTableWidget.
  • Now, go to the Attributes Inspector pane and then at the top, specify the attribute to be equal to category. This will associate the refinementList with the attribute category.
  • Next, go to the RefinementViewController.swift class, and then add protocol , RefinementTableViewDataSource next to UIViewController.
  • Add the following property below the declared tableView:
1
var refinementController: RefinementController!

This refinementController will take care of the dataSource and delegate methods of the tableView, and will provide other advanced dataSource and delegate methods that contain information about the refinement. To achieve that, add the following in your viewDidLoad method:

1
2
3
4
5
6
7
refinementController = RefinementController(table: tableView)
tableView.dataSource = refinementController
tableView.delegate = refinementController
refinementController.tableDataSource = self
// refinementController.tableDelegate = self

InstantSearch.shared.register(widget: tableView)

Remember at the end to always register the widget to InstantSearch so that it receives interesting search events from it. Finally, we need to add our dataSource method to specify the look and feel of the refinementList cell.

1
2
3
4
5
6
7
8
9
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing facet: String, with count: Int, is refined: Bool) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "refinementCell", for: indexPath)

    cell.textLabel?.text = facet
    cell.detailTextLabel?.text = String(count)
    cell.accessoryType = refined ? .checkmark : .none

    return cell
}

We’re done! Build and run your application: you now have an InstantSearch iOS app that can filter data!. Click on Filter and select refinements then go back. You will see that the stats and hits widget automatically update with the new data. Sweet! You can also go to your Main.storyboard and play around with the custom parameters of each widget available in the attributes inspector.

In this part, you’ve learned:

  • How to add the RefinementList widget
  • How to configure the widget
  • How to specify the look and feel of your refinement cells

Display data from multiple hits

It is probable that your app is targeting multiple indices: for example, you want to search through bestbuy items as well as movies items. Let’s see how this is possible.

First make sure that you’re using the latest version of InstantSearch (after 2.1.0).

Code

In order to specify to InstantSearch which index you want to target, go to your AppDelegate and then inside your application(_:didFinishLaunchingWithOptions:) method, replace its contents with:

1
2
let searcherIds = [SearcherId(index: "bestbuy_promo"), SearcherId(index: "movies")]
InstantSearch.shared.configure(appID: ALGOLIA_APP_ID, apiKey: ALGOLIA_API_KEY, searcherIds: searcherIds)

In that way, InstantSearch is aware of the indices that it has to deal with. Next, go to your ViewController and replace all of it with the following:

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
import UIKit
import InstantSearch
import InstantSearchCore

class HitsViewController: MultiHitsTableViewController {

    @IBOutlet weak var tableView: MultiHitsTableWidget!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        hitsTableView = tableView

        InstantSearch.shared.registerAllWidgets(in: self.view)
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing hit: [String : Any]) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "hitCell", for: indexPath)

        if indexPath.section == 0 { // bestbuy
            cell.textLabel?.highlightedTextColor = .blue
            cell.textLabel?.highlightedBackgroundColor = .yellow
            cell.textLabel?.highlightedText = SearchResults.highlightResult(hit: hit, path: "name")?.value
        } else { // movies
            cell.textLabel?.highlightedTextColor = .white
            cell.textLabel?.highlightedBackgroundColor = .black
            cell.textLabel?.highlightedText = SearchResults.highlightResult(hit: hit, path: "title")?.value
        }

        return cell
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 30))

        let view = UIView()
        view.addSubview(label)
        view.backgroundColor = UIColor.gray
        return view
    }
}

Things to note:

  • First, from the previous single-index implementation, we changed some classes from HitsXYZ to MultiHitsXYZ. These are MultiHitsTableViewController and MultiHitsTableWidget.
  • Nothing has changed in the ViewDidLoad, and in the cellForRowAt method, we now have to deal with 2 sections since we’ll have one for bestbuy and one for movies.
  • Finally, we specify a viewForHeaderInSection to put a separator between these 2 sections.

Storyboard

Head into the Main.Storyboard, select your stat widget, and then in the property inspector, add movies to the index field. This specifies that the stats label shows a number of results for movies. Next, click on the tableView which shows the hits, and then in the identity inspector, change the class to MultiHitsTableWidget. Then, head to the identity inspector and specify the following:

  • Indices: bestbuy_promo,movies
  • Hits Per Section: 5,10

Here, we are saying that we want to show the bestbuy_promo index in the first section and the movies index in the second. We also specify that we want 5 hits to appear for the bestbuy_promo index and 10 for the movies index.

Great, now run your app, search in the search bar and you should see results appearing from the indices!

There’s still one more thing: If you click on the filter button, the app will crash. Why is that? This is because we are in “multi-index” mode and the refinement list doesn’t have a clue what index to target: is it bestbuy_promo or movies?

In order to specify this, go to your main.storyboard class, then click on the refinement list. In the attribute inspector, specify bestbuy_promo as the index.

Now go ahead and run your app again. When going to the filters screen and selecting a filter, you’ll notice in the main screen that the refinements are only being applied to the bestbuy_promo index.

In this part, you’ve learned:

  • How to configure InstantSearch for Multi-indexing
  • How to use the MultiHitsTableWidget which can show different indices
  • How to specify an index for widgets such as the StatsLabelWidget and RefinementTableWidget

Notes

  • You might have seen a Variant or Variants attributes in the identity inspector (variants example: main,details). These are used in case you want 2 widgets to use the same index but with different configurations. You also have to specify those variants when configuring InstantSearch using the SearcherId(index:variant) constructor instead of SearcherId(index:).
  • Concerning the SearchBarWidet: by not specifying any index there, InstantSearch will conclude that the Search bar should search in all index. If an index was specified, then the SearchBarWidget would only trigger a search in that particular widget.

Go further

Your application now displays your data from multiple indices, lets your users enter a query, displays search results as-they-type, and lets users filter by refinements: you just built a full instant-search interface! Congratulations 🎉

This is only an introduction to what you can do with InstantSearch iOS:

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

Did you find this page helpful?

iOS InstantSearch v3