Welcome to our Learn At Home Sale!

Limited-time Advanced Swift & Android book bundles, plus 50% off all books

Home · iOS & Swift Tutorials

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.

5/5 4 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

UISearchBar and UISearchController are staples of iOS app development. But while UISearchBar has received periodic changes since its introduction in iOS 2, UISearchController has been pretty static since Apple introduced it in iOS 8. In iOS 13, Apple updated both.

Apple also introduced UISearchToken, which provides much-needed power to UISearchController. With very little effort, you can enable your users to perform complex search queries in addition to the text-based searches they’re used to.

If you’ve ever written tedious, fragile code to traverse the search bar’s view hierarchy to get a reference to the search text field, there is more good news. The UISearchTextField is now exposed as a property, making customization much easier.

In this tutorial, you’ll learn:

  • How to control the search results controller’s display.
  • Everything there is to know about search tokens.
  • How to create and use search tokens.
  • How to customize UISearchController, the search bar and its text field.
Note: If UISearchController is new to you, read UISearchController Tutorial: Getting Started first.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Throughout this tutorial, you’ll work on L.I.S.T.E.D., the Large International Sorted Tally of Earth Dwellers. L.I.S.T.E.D. is an organization that keeps track of the total population of the world. They currently have populations for all countries for the years 2018 and 2019.

Anticipating the release of 2020 population data, they want to expand their search capabilities. As an Earth dweller yourself, you’re going to help refactor the app and bring its search functionality to a whole new level for the organization.

Start by opening LISTED.xcodeproj inside the starter folder, then open Main.storyboard.

The storyboard for the app

You’ll see the app is pretty simple. There are only two view controllers, which are wrapped inside a navigation controller.

Build and run. You’ll see this:

The app prior to any modifications. It displays a table view with a list of countries and population data

Now, search for “new”. You’ll see these search results:

Search results for countries that contain the word 'new' showing population data for 2018 and 2019.

The result displays matches for all years. Tapping 2018 or 2019 in the scope bar will narrow the search. Try it now by tapping 2019 and this is what you’ll see:

Search results for countries that contain the word 'new' showing only 2019 population data.

Great, basic searching works fine. Now, you’ll make it better!

Using the Search Results Controller

UISearchController provides two options for displaying results: displaying them in the same view that shows the original data or using a search results controller.

L.I.S.T.E.D. uses a search results controller to display the results in a slightly different format from the main controller. When the user taps into the search bar, the main view controller remains visible. The result view controller displays after the user starts typing the search.

Before iOS 13, you had little control over this behavior, but now you can use the newly-added showsSearchResultsController in UISearchController to customize your results. You’ll see how in the next sections.

Displaying Results: You’re in Control

To control when search results display, you need to react to changes in the search bar. For this to work, the main view controller will conform to UISearchResultsUpdating.

The delegate receives a call to updateSearchResults(for:) when the search bar becomes the first responder or when text changes. You’ll use this to trigger the display of the results controller.

Open MainViewController.swift and add the following after UISearchBarDelegate:

// MARK: -

extension MainViewController: UISearchResultsUpdating {
  func updateSearchResults(for searchController: UISearchController) {
    searchController.showsSearchResultsController = true
  }
}

The search results now display when the search bar becomes the first responder.

Before you can test that, add the following code as the last line in viewDidLoad():

searchController.searchResultsUpdater = self

The search results updater is responsible for updating the search results controller. Here, you’re assigning that responsibility to the main controller.

Build and run. Tap the search bar and you’ll see an empty search results controller.

An empty search results view.

You’re now in full control of showing — and hiding — the search results. As powerful as you must feel now, showing a blank results controller is not a great user experience. You’ll address that soon, but first, you need to know more about search tokens.

Everything You Need to Know About Search Tokens

Search tokens are arguably the most interesting feature Apple added to search in iOS 13. If you use Apple’s Mail or Photos apps on iOS 13, you’ve likely already seen search tokens in action.

The Mail app uses search tokens to create complex searches. Tapping the search bar shows suggestions like “unread messages” and “flagged messages”.

Apple's Mail app displaying the search results view. The view displays a list of suggested searches.

Tokens can represent complex searches like searching by geolocation or simple searches using predetermined text. The key to search tokens is representedObject in UISearchToken.

representedObject is an Any? and can contain any type of data that’s useful to you.

It’s important to keep in mind that representedObject is strongly referenced. Any object it holds may stick around for quite some time. Use lightweight data to avoid problems. Strings, Ints and NSManagedObjectIDs are great candidates.

Creating Tokens

It’s time to address the empty search results view you see when you tap the search bar. This is a great place to show a list of tokens available to your users.

Open ResultsTableViewController.swift. At the top of the class, after countries, add the following:

var searchTokens: [UISearchToken] = []

Here, you’re creating an array to hold the search tokens. After that line, add the following:

var isFilteringByCountry: Bool {
  return countries != nil
}

This computed Boolean will return true when users are searching or false when they’re not. You’ll use it soon.

At the end of the file, below the class, add the following extension:

// MARK: -

extension ResultsTableViewController {
  func makeTokens() {
    // 1
    let continents = Continent.allCases
    searchTokens = continents.map { (continent) -> UISearchToken in
      // 2
      let globeImage = UIImage(systemName: "globe")
      let token = UISearchToken(icon: globeImage, text: continent.description)
      // 3
      token.representedObject = Continent(rawValue: continent.description)
      // 4
      return token
    }
  }
}

This code does the following:

  1. Creates an array of all continents.
  2. Creates an image that represents the token. Next, using the image and current continent’s description, it creates a search token.
  3. Assigns the continent’s description to the token’s representedObject. You’ll use this later to narrow searches to specific continents. Using a lightweight value such as a string is perfect for this situation.
  4. Returns the token, which appends to searchTokens, which you created earlier.

In viewDidLoad(), add this as the last line:

makeTokens()

Here, you create the search tokens when the view loads. That’s it! Creating search tokens is that simple. :]

Happy iPhone with search icon

Before you build and run again, you need to update the results controller to display these new tokens. You’ll do that in the next step.

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.

Hiding the Scope Bar

You still have a few more things to do before you can call this ready for production. The L.I.S.T.E.D. design team has determined that the scope bar shouldn’t be visible when the results controller is showing the token selection UI.

To implement this, go to viewDidLoad() and add this as the last line:

searchController.automaticallyShowsScopeBar = false

Prior to iOS 13, the scope bar would always display automatically. Now, you can control this behavior by using the new automaticallyShowsScopeBar.

Note: there is a similar automaticallyShowsCancelButton. This property allows you to control the search bar’s cancel button visibility. While you don’t need it for this project, you should be aware that it exists.

Next, find selectedScopeYear() and add the following after it:

func showScopeBar(_ show: Bool) {
  guard searchController.searchBar.showsScopeBar != show else { return }
  searchController.searchBar.setShowsScope(show, animated: true)
  view.setNeedsLayout()
}

Here, you check if the search bar’s showsScopeBar matches show. If it does, you’ll stop because there’s nothing to do.

If it doesn’t match, you use the new setShowsScope(_:animated:) to show or hide the scope bar.

Finally, you must call setNeedsLayout() on the view controller’s view.

You’ll now use this new function to show and hide the scope bar. In UISearchBarDelegate, find searchBar(_:textDidChange:) and add the following as the last lines:

let showScope = !searchText.isEmpty
showScopeBar(showScope)

If the search text is not empty, the scope bar should be shown.

In searchBarCancelButtonClicked(_:), add the following as the last line:

showScopeBar(false)

Now, when the user taps the search bar’s Cancel button, you’ll hide the scope bar.

Finally, in ResultsTableViewDelegate, add the following to the end of didSelect(token:):

showScopeBar(true)

When the user selects a token, you’ll now show the scope bar.

Build and run, then tap the search bar. You’ll no longer see the scope bar.

The search results view with a hidden scope bar.

Tap a token and the scope bar will appear.

The search results view with a visible scope bar.

Customizing the Search Bar and Text Field

Your final task is to add a theme to the search bar. Before exposing the search text field in iOS 13, customizing the field was fraught with issues. Now, with the text field exposed, you can customize it like any other UITextField.

Changing Text and Background Color

The design team wants the text field to stand out a bit more, so your first task is to change the color of the text.

In viewDidLoad(), add this as the last line:

searchController.searchBar.searchTextField.textColor = .rwGreen()

The project has a UIColor extension. This extension returns a very specific green, used by your favorite tutorial site. Here. you’re setting the search text field’s text color to that gorgeous shade of green.

Next on the list is to change the background color. When the search bar becomes the first responder, it should become a transparent green. When the user cancels the search, it should return to its default color.

To implement this, find updateSearchResults(for:) in the UISearchResultsUpdating extension and replace it with:

func updateSearchResults(for searchController: UISearchController) {
  // 1
  if searchController.searchBar.searchTextField.isFirstResponder {
    searchController.showsSearchResultsController = true
    // 2
    searchController.searchBar
      .searchTextField.backgroundColor = UIColor.rwGreen().withAlphaComponent(0.1)
  } else {
    // 3
    searchController.searchBar.searchTextField.backgroundColor = nil
  }
}

Here, you’re:

  1. Showing the results controller if the search text field is the first responder.
  2. Changing the search text field’s background color to rwGreen. You use an alpha component to make the color transparent.
  3. If the search text field is not the first responder, you set the background color back to its default.

In UISearchDelegate, find searchBarCancelButtonClicked(_:) and add this as the last line:

searchController.searchBar.searchTextField.backgroundColor = nil

If the user cancels the search, you’ll set the text field’s background to the default.

Build and run. Tap the search bar, tap Search by Africa and type “faso”. You’ll see the following:

The search results with a search text field. The field has green text on a light green background.

You’ve now set the search text field’s text and background theme. Things are looking sharp!

But, now the token doesn’t look quite right. There’s always something, isn’t there? Don’t worry, you’ll fix that next.

Changing the Color of Tokens

Tokens have a few theming options. For example, you can set the token’s icon, as you did earlier. You can also change the background color, which is what you’ll do next.

In viewDidLoad(), add this as the last line:

searchController.searchBar.searchTextField.tokenBackgroundColor = .rwGreen()

Here, you’re setting the default background color for tokens to rwGreen.

Build and run. Tap search and tap Search by Oceania and you’ll see this:

The search results with a search text field. The field has a green search token.

Look at all that green! It really helps your fantastic new search functionality stand out.

Where to Go From Here?

You can download the completed project using the Download Materials button at the top or bottom of this article.

Congratulations! You now have the latest UISearchController functionality in your app.

While refactoring the app, you’ve learned how to:

  • Control displaying the search results view controller.
  • Use search tokens.
  • Control the visibility of the scope bar.
  • Customize the newly-exposed search text field.
  • Customize search tokens.

Whew! That’s a lot. Take a look at these great resources to learn even more about UISearchController:

Please share any comments or questions about this article in the forum discussion below!

Average Rating

5/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments