Concepts / Building Search UI / Customize an existing widget
Jan. 07, 2019

Customize an existing widget

Highlight and snippet your search results

Highlighting

Visually highlighting the search result is an essential feature of a great search interface. It will help your users understand your results by showing them why a result is relevant to their query.

In order to add highlighting to your Hits widget, we offer a utility method on UILabel stored properties. There are a few attributes that you can specify to your highlighted label:

  • isHighlightingInversed: whether the highlighting is reversed or not.
  • highlightedTextColor : The text color of the highlighting (optional).
  • highlightedBackgroundColor : The background color of the highlighting (optional).
  • highlightedText : The text that is highlighted. Here we need to use the utility method: SearchResults.highlightResult(hit: hit, path: "your_attribute")?.value

Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
import InstantSearch

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

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

        return cell
    }

Highlighting using InstantSearchCore

The Highlighter class is in charge of parsing highlight result values (as returned by the Search API in the _highlightResults attribute of every hit) and render them into a rich text string (an NSAttributedString instance).

Styles

When you instantiate a highlighter, you specify a set of text attributes that will be applied to highlighted portions. For example, the following code will give you truly ugly red-on-yellow highlights:

1
2
3
4
let highlighter = Highlighter(highlightAttrs: [
    NSForegroundColorAttributeName: UIColor.red,
    NSBackgroundColorAttributeName: UIColor.yellow,
]

Tags

By default, the highlighter is set to recognize <em> tags, which are the default tags used by the Search API to mark up highlights. However, you can easily override that to a custom value.

Note: In that case, make sure that it matches the values for highlightPreTag and highlightPostTag in your search query (or your index’s default)!

1
2
highlighter.preTag = "<mark>"
highlighter.postTag = "</mark>"

Rendering

Once the highlighter is configured, rendering highlights is just a matter of calling render(text:). The real trick is to retrieve the highlighted value from the JSON. Fortunately, the SearchResults class makes this easy:

1
2
3
4
5
6
7
let searchResults: SearchResults = ... // whatever was received by the result handler
let index: Int = ... // index of the hit you want to retrieve
if let highlightResult = searchResults.highlightResult(at: index, path: "attribute_name") {
    if let highlightValue = highlightResult.value {
        let highlightedString = highlighter.render(text: highlightValue)
    }
}

Inverse highlighting

In most cases, you want to highlight parts of the text that matched the search query, to show users why the results are relevant to their search. There may be cases, however, where inverse highlighting is more adapted.

Let’s consider query suggestions: as the user types, you are suggesting queries that contain the text already entered. In that case, highlighting the matched text does not bring any useful information, as it is the same for all suggestions. What is much more relevant is to highlight the remaining parts, i.e. the additional text supplied by each suggestion.

For example, when searching for “star”, instead of displaying:

  • star wars
  • star trek

… you could display:

  • star wars
  • star trek

This is the goal of the inverseHighlights(in:) function. Just supply a string with regular highlights, and it will convert highlighted parts into non-highlighted parts and vice versa.

1
2
3
let highlighter = /* your highlighter */
print(highlighter.inverseHighlights(in: "<em>star</em> wars"))
// ... will print `star<em> wars</em>`

Style your widgets

All the widgets found in the InstantSearch library inherit from UIKit UI components such as UITableView, UISlider or UITextField.

Therefore, you’ll be able to customize the look and feel of your widgets the same way you would customize UIKit components.

Translate your widgets

Most of the widgets found in the InstantSearch library offer ways for you to customize the look and feel of your output. Here is an example of customizing the rows of a hits table widget:

1
2
3
4
5
6
7
8
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing hit: [String : Any]) -> UITableViewCell {
        let cell = hitsTableView.dequeueReusableCell(withIdentifier: "hitTableCell", for: indexPath)

        // Can specify anything here. Can also translate to another language.
        cell.textLabel?.text = hit["name"] as? String

        return cell
    }

Modify the list of items in widgets

If you want to access the list of items inside a widget, you can do it throught the searcher. Each Searcher stores the latest hits in searcher.hits. You can potentially use this to build your own array of hits, and then manipulate it.

You can access the searcher of an index with InstantSearch.shared.getSearcher(named:id:) if you’re in multi-index mode, or just InstantSearch.shared.searcher if you’re only using a single index.

You can also use the result handler callback of a searcher to get the hits from the results, and then you can manipulate them the way you want. To do this, you just call searcher.addResultHandler(resultHandler:)

Apply default value to widgets

In order to add a default refinement to a specific index, first you need to access the searcher of that index with InstantSearch.shared.getSearcher(named:id:) if you’re in multi-index mode, or just InstantSearch.shared.searcher if you’re only using a single index.

Then, you can add the default refinements with one of the params methods of the searcher, for example: searcher.params.addFacetRefinement(name: "type", value: "book") or searcher.params.addNumericRefinement("price", .lessThanOrEqual, 200).

How to provide search parameters

Algolia has a wide range of parameters. If one of the parameters you want to use is not covered by any widget or viewmodel, then you can use the InstantSearch.shared.params field.

Basic Search Parameters

Most of the Algolia API parameters are available through properties of InstantSearch.shared.params. For example, in order to modify the attributesToRetrieve, you can do the following:

1
InstantSearch.shared.params.attributesToRetrieve = ["att1", "att2"]

Filtering

Facets

The params property maintains a list of refined values for every facet, called facet refinements. A facet refinement is the combination of an attribute name and a value. Optionally, the refinement can be negated (i.e. treated as exclusive rather than inclusive).

To edit the refinements, use the facet refinement handling methods, like addFacetRefinement(name:value:), removeFacetRefinement(name:value:) and toggleFacetRefinement(name:value:). Let’s give an example:

1
InstantSearch.shared.params.addFacetRefinement(name: "facetName", value: "facetValue")

A given facet can be treated as either conjunctive (the default—refinements combined with an AND operator) or disjunctive (refinements combined with an OR). You can modify the conjunctive/disjunctive status of a facet by calling setFacet(withName:disjunctive:).

When a search is triggered, the searcher will build the facet filters according to the refinements and the conjunctive/disjunctive state of each facet.

Note: You still need to specify the list of all facets via the facets search parameter.

Note: The filters and facetFilters search parameters will be overridden by the facet refinements; any manually specified value will be lost.

Numeric filters

The search parameters also provide tools to easily manipulate numeric filters, through the notion of numeric refinements. A numeric refinement is basically made of an attribute name (the left operand), a comparison operator, and a value (right operand). Optionally, the expression can be negated.

The numeric refinement handling methods work in a very similar fashion to the facet refinements (see above):

  • A given numeric attribute can be treated as either conjunctive (the default) or disjunctive. The conjunctive/disjunctive status is modified via setNumeric(withName:disjunctive:).

  • Numeric refinements are edited via addNumericRefinement(...) and removeNumericRefinement(...). Let’s give an example:

1
InstantSearch.shared.params.addNumericRefinement("price", .greaterThanOrEqual, 10)

Note: The filters and numericFilters search parameters will be overridden by the numeric refinements; any manually specified value will be lost.

Customize the complete UI of the widgets

InstantSearch iOS provides out-of-the-box widgets which are great when you want a default style to be applied and you do not need heavy customization of the rendering or behavior of the widget. But when these widgets are not enough, InstantSearch iOS provides a lower-level API called the ViewModels.

As soon as you hit a feature wall using our default widgets, you can use ViewModels to have more flexibility. ViewModels encapsulate the logic for a specific search concept and provide a way to interact with InstantSearch.

When do I need to use ViewModels?

  • When you want to display our widgets using another UI library like Material-components
  • When you want to have full control on the rendering without having to reimplement business logic
  • As soon as you hit a feature wall using our default widgets

SearchViewModel

Use this to customize any kind of search box.

Methods
  • search(query:)
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@IBOutlet weak var searchBar: SearchBarWidget!
var searchViewModel: SearchViewModel!

override func viewDidLoad() {
        super.viewDidLoad()

        searchViewModel = SearchViewModel(view: searchBar)
        InstantSearch.shared.register(viewModel: searchViewModel)

        // Now can access access the searchBar's delegate
        searchBar.delegate = self
    }

    public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        // Call the search function of the viewModel. It will take care of changing the
        // search state and sending search events when new results arrive.
        searchViewModel.search(query: searchText)
    }

HitsViewModel

Use this to customize a hits view with only 1 index.

Methods

  • numberOfRows()
  • hitForRow(at:)

Example

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
@IBOutlet weak var tableView: HitsTableWidget!
var hitsViewModel: HitsViewModel!

override func viewDidLoad() {
        super.viewDidLoad()

        hitsViewModel = HitsViewModel(view: tableView)
        InstantSearch.shared.register(viewModel: hitsViewModel)

        // Now can access access the tableView's delegate and datasource methods.
        tableView.dataSource = self
        tableView.delegate = self
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return hitsViewModel.numberOfRows()
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        let hit = hitsViewModel.hitForRow(at: indexPath)
        cell.textLabel?.text = getTextOutOfHit(hit)

        return cell
    }

MultiHitsViewModel

Use this to customize a hits view with only multiple indices.

Methods

  • numberOfRows(in:)
  • hitForRow(at:)
  • numberOfSections()

Example

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
@IBOutlet weak var tableView: MultiHitsTableWidget!
var multiHitsViewModel: MultiHitsViewModel!

override func viewDidLoad() {
        super.viewDidLoad()

        multiHitsViewModel = MultiHitsViewModel(view: tableView)
        InstantSearch.shared.register(viewModel: multiHitsViewModel)

        // Now can access access the tableView's delegate and datasource methods.
        tableView.dataSource = self
        tableView.delegate = self
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return multiHitsViewModel.numberOfSections()
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return multiHitsViewModel.numberOfRows(in: section)
    }

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let hit = multiHitsViewModel.hitForRow(at: indexPath)

        if indexPath.section == 0 { // First index
            cell.textLabel?.text = getTextOutOfHitWithFirstIndex(hit)
        } else { // Second index
            cell.textLabel?.text = getTextOutOfHitWithSecondIndex(hit)
        }

        return cell
    }

NumericControlViewModel

Use this to customize any kind of numeric control view.

Methods

  • updateNumeric(value:doSearch:)
  • removeNumeric(value:)

FacetControlViewModel

Use this to customize any kind of facet control view.

Methods

  • addFacet(value:doSearch:)
  • updatefacet(oldValue:newValue:doSearch:)
  • removeFacet(value:)

RefinementMenuViewModel

Use this to customize any kind of refinement menu view.

Methods

  • numberOfRows()
  • facetForRow(at:)
  • isRefined(at:)
  • didSelectRow(at:)

Events

The Searcher found in InstantSearch.shared.getSearcher() class emits notifications through NSNotificationCenter on various events of its lifecycle:

  • Searcher.SearchNotification when a new request is fired
  • Searcher.ResultNotification when a successful response is received
  • Searcher.ErrorNotification when an erroneous response is received
  • SearcherRefinementChangeNotification when numeric and facet refinements are changed

You may subscribe to these notifications to react on different events without having to explicitly write a result handler.

Did you find this page helpful?