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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
What’s New With UISearchController and UISearchBar
25 mins
- Getting Started
- Using the Search Results Controller
- Displaying Results: You’re in Control
- Everything You Need to Know About Search Tokens
- Creating Tokens
- Making a UI for Selecting Tokens
- Adding Tokens to the Search Bar
- Modifying Your Search to Use Tokens
- Hiding the Scope Bar
- Customizing the Search Bar and Text Field
- Changing Text and Background Color
- Changing the Color of Tokens
- Where to Go From Here?
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:
- 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’scountry
and return the cell. - Otherwise, you use a
SearchTokenCell
. You assign the token to the cell’stoken
and return the cell. - 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.
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:
- Get the search bar’s text field.
- Use the field’s
insertToken(_:at:)
to add the token to the end of tokens already in the field. - 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:
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.
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:
- Create an array of tokens contained in the search bar’s text field.
- 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:
- If the search controller is not currently active, it will terminate.
- If the search text is
nil
, you set the result controller’scountries
tonil
and terminate. - Get the selected year from the scope bar. Next, create an array of all countries and start filtering the array.
- 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. - Using
searchContinents
, which you created earlier, create a Boolean if the country’s continent matches any selected tokens. You returntrue
ifsearchContinents
isnil
becausenil
means that you match all continents. - 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.
- If you have tokens but no text, you return the country if it matches the year and the token’s continent.
- When neither of those two cases is true, you’ll return
false
. - Assign any filtered countries to the result controller’s
countries
. If there aren’t any, assignnil
.
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.
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:
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.