What’s New With UISearchController and UISearchBar

In this UISearchController tutorial, you’ll learn about UISearchToken, UISearchTextField and other new APIs introduced in iOS 13. By Corey Davis.

4.9 (11) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Making a UI for Selecting Tokens

Now, you’ll create a UI for selecting tokens using the Mail app as inspiration. To begin, replace tableView(_:numberOfRowsInSection:) with the following:

override func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  return isFilteringByCountry ? (countries?.count ?? 0) : searchTokens.count
}

Using isFilteringByCountry, which you created earlier, you determine whether to use the count of search tokens or count of countries to set the number of rows in the table view. If the user is searching, you send the country count (or zero if countries is nil). When they aren’t searching, you send the token count.

Next, replace tableView(_:cellForRowAt:) with the following:

override func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  // 1
  if
    isFilteringByCountry,
    let cell = tableView.dequeueReusableCell(
      withIdentifier: "results",
      for: indexPath) as? CountryCell {
    cell.country = countries?[indexPath.row]
    return cell
  
  // 2
  } else if
    let cell = tableView.dequeueReusableCell(
      withIdentifier: "search",
      for: indexPath) as? SearchTokenCell {
    cell.token = searchTokens[indexPath.row]
    return cell
  }

  // 3
  return UITableViewCell()
}

Here’s what you do with this code:

  1. You first check to see if the user is searching for a country. If so, you use CountryCell. You then assign the country to the cell’s country and return the cell.
  2. Otherwise, you use a SearchTokenCell. You assign the token to the cell’s token and return the cell.
  3. If all else fails, you return a UITableViewCell.

Build and run. Tap the search bar but don’t type any text. You’ll see the UI you created to select the search tokens.

The app opens to the search results view and shows the new search tokens.

This is coming along wonderfully, but there’s a problem: If you tap one of the tokens, nothing happens. Boo! Not to worry, you’ll fix that next.

Adding Tokens to the Search Bar

When you tap one of the token entries in the results view, nothing happens. What should happen is that the token gets added to the search bar. This indicates to users that they’re searching within the continent they specified.

The results controller cannot add the token because it isn’t the owner of the search bar. When the user taps a token, you must notify the main view controller. To do this, you’ll use a delegate protocol.

Start by adding the following code to the top of ResultsTableViewController.swift before the class:

protocol ResultsTableViewDelegate: class {
  func didSelect(token: UISearchToken)
}

At the top of the class, after isFilteringByCountry, add the following:

weak var delegate: ResultsTableViewDelegate?

You’ve created a simple protocol that you’ll use to inform the delegate when the user taps a token. Now, add the following code after tableView(_:cellForRowAt:):

override func tableView(
  _ tableView: UITableView,
  didSelectRowAt indexPath: IndexPath
) {
  guard !isFilteringByCountry else { return }
  delegate?.didSelect(token: searchTokens[indexPath.row])
}

First, you check if the view is showing tokens or countries. If it’s a country, you ignore the row selection. Otherwise, you inform the delegate which search token the user tapped.

In MainViewController.swift, add the following to the bottom of the file:

// MARK: -

extension MainViewController: ResultsTableViewDelegate {
  func didSelect(token: UISearchToken) {
    // 1
    let searchTextField = searchController.searchBar.searchTextField
    // 2
    searchTextField.insertToken(token, at: searchTextField.tokens.count)
    // 3
    searchFor(searchController.searchBar.text)
  }
}

When notifying the main view controller the user has selected a token, you:

  1. Get the search bar’s text field.
  2. Use the field’s insertToken(_:at:) to add the token to the end of tokens already in the field.
  3. Run the search algorithm.

In viewDidLoad(), after the instantiation of resultsTableViewController, add:

resultsTableViewController.delegate = self

Now, the main view controller will be the results controller’s delegate.

Build and run then tap the search bar. When the results controller appears, tap “Search by Europe”. Then, type “united” and you’ll see this:

The search results showing results for the 'united' search using the Europe token. The search results are incorrect.

Exciting! You’ve added a search token to the search bar. However, it doesn’t seem to be working.

Unless geography has changed since you were in high school, the United States and the United Arab Emirates are not in Europe. So what gives?

The problem is in the search algorithm: You haven’t updated it to take tokens into consideration. Fixing this is your next challenge.

Yeti with magnifier image.

Modifying Your Search to Use Tokens

The current search algorithm uses the search bar’s text and scope to perform a search. You’ll refactor it to use tokens as well.

Before doing that, you need to create a couple of helper properties. In MainViewController.swift, add the following after resultsTableViewController:

var searchContinents: [String] {
  // 1
  let tokens = searchController.searchBar.searchTextField.tokens
  // 2
  return tokens.compactMap {
    ($0.representedObject as? Continent)?.description
  }
}

This computed property will:

  1. Create an array of tokens contained in the search bar’s text field.
  2. Return an array of continent strings using each token’s representedObject.

Add this code after the searchContinents:

var isSearchingByTokens: Bool {
  return
    searchController.isActive &&
    searchController.searchBar.searchTextField.tokens.count > 0
}

This property returns true if the search controller is active and the search bar contains search tokens.

Use these new properties by replacing searchFor(_:) with:

func searchFor(_ searchText: String?) {
  // 1
  guard searchController.isActive else { return }
  // 2
  guard let searchText = searchText else {
    resultsTableViewController.countries = nil
    return
  }
  // 3
  let selectedYear = selectedScopeYear()
  let allCountries = countries.values.joined()
  let filteredCountries = allCountries.filter { (country: Country) -> Bool in
    // 4
    let isMatchingYear = selectedYear == Year.all.description ? 
      true : (country.year.description == selectedYear)
    // 5
    let isMatchingTokens = searchContinents.count == 0 ? 
      true : searchContinents.contains(country.continent.description)
    // 6
    if !searchText.isEmpty {
      return
        isMatchingYear &&
        isMatchingTokens &&
        country.name.lowercased().contains(searchText.lowercased())
    // 7
    } else if isSearchingByTokens {
      return isMatchingYear && isMatchingTokens
    }
    // 8
    return false
  }
  // 9
  resultsTableViewController.countries = 
    filteredCountries.count > 0 ? filteredCountries : nil
}

The new search algorithm does the following:

  1. If the search controller is not currently active, it will terminate.
  2. If the search text is nil, you set the result controller’s countries to nil and terminate.
  3. Get the selected year from the scope bar. Next, create an array of all countries and start filtering the array.
  4. Countries appear once per year. With data for 2018 and 2019, each country is listed twice. Your first step when filtering is to create a Boolean, which is true if the selected year is “all”. If not, you return a Boolean based on whether the year for the country is equal to the selected year.
  5. Using searchContinents, which you created earlier, create a Boolean if the country’s continent matches any selected tokens. You return true if searchContinents is nil because nil means that you match all continents.
  6. If there’s any search text, you’ll return the country if the year matches, the tokens match and the country’s name contains any of the characters in the search text.
  7. If you have tokens but no text, you return the country if it matches the year and the token’s continent.
  8. When neither of those two cases is true, you’ll return false.
  9. Assign any filtered countries to the result controller’s countries. If there aren’t any, assign nil.

Build and run. Tap the search bar and, like last time, tap Search by Europe and enter united. You’ll only see entries for the United Kingdom in 2018 and 2019.

The search results for 'united' using the Europe token. The search results are correct.

While this is amazing, don’t celebrate just yet. Make sure that the changes you made haven’t affected the ability to search without tokens.

Select the Europe token in the search text field and delete it without deleting the word “united”. You’ll see:

The search results for 'united' with no token.

Wow! You’ve done some great work. With only a few small changes, you have drastically transformed L.I.S.T.E.D.’s search capabilities.