UISearchController Tutorial Getting Started
UISearchController Tutorial Getting Started
raywenderlich.com/4363809-uisearchcontroller-tutorial-getting-started
Getting Started
Start by downloading the starter project using the Download Materials button at the top
or bottom of this tutorial. Once it’s downloaded, open CandySearch.xcodeproj in Xcode.
To keep you focused, the starter project has everything unrelated to searching and
filtering already set up for you.
The view controller on the left is the root navigation controller of the app. Then you have:
1. MasterViewController: This contains the table view that you’ll use to display and
filter the candies you’re interested in.
2. DetailViewController: This displays the details of the selected candy along with its
image.
Build and run the app and you’ll see an empty list:
Back in Xcode, the file Candy.swift contains a struct to store the information about each
piece of candy you’ll display. This struct has two properties:
1/16
When the user searches for a type of candy in
your app, you’ll search the name property
using the user’s query string. The category will
become important near the end of this
tutorial, when you implement the scope bar.
Note: In this tutorial, you only need to create a limited number of values to illustrate how
the search bar works; in a production app, you might have thousands of these searchable
objects. But whether an app has thousands of objects to search or just a few, the methods
you use will remain the same. This is scalability at its finest!
To populate candies , add the following code to viewDidLoad() after the call to
super.viewDidLoad() :
candies = Candy.candies()
Build and run. Since the sample project has already implemented the table view’s data
source methods, you’ll see that you now have a working table view:
Selecting a row in the table will also display a detail view of the corresponding candy:
So much candy, so little time to find what you want! You need a UISearchBar .
2/16
3/16
Introducing UISearchController
If you look at UISearchController ‘s documentation, you’ll discover it’s pretty lazy. It
doesn’t do any of the work of searching at all. The class simply provides the standard
interface that users have come to expect from their iOS apps.
UISearchController communicates with a delegate protocol to let the rest of your app
know what the user is doing. You have to write all of the actual functionality for string
matching yourself.
Although this may seem scary at first, writing custom search functions gives you tight
control over how your specific app returns results. Your users will appreciate searches
that are intelligent and fast.
If you’ve worked with searching table views in iOS in the past, you may be familiar with
UISearchDisplayController . Since iOS 8, Apple has deprecated this class in favor of
UISearchController , which simplifies the entire search process.
4/16
By initializing UISearchController with a nil value for
searchResultsController , you’re telling the search controller that you want to use the
same view you’re searching to display the results. If you specify a different view controller
here, the search controller will display the results in that view controller instead.
Still in MasterViewController.swift, add the following class extension outside of the main
MasterViewController :
// TODO
updateSearchResults(for:) is the one and only method that your class must
implement to conform to the UISearchResultsUpdating protocol. You’ll fill in the
details shortly.
Next, you need to set up a few parameters for your searchController . Still in
MasterViewController.swift, add the following to viewDidLoad() , just after the
assignment to candies :
// 1
searchController.searchResultsUpdater = self
// 2
searchController.obscuresBackgroundDuringPresentation = false
// 3
// 4
navigationItem.searchController = searchController
// 5
definesPresentationContext = true
5/16
4. New for iOS 11, you add the searchBar to the navigationItem . This is
necessary because Interface Builder is not yet compatible with
UISearchController .
5. Finally, by setting definesPresentationContext on your view controller to
true , you ensure that the search bar doesn’t remain on the screen if the user
navigates to another view controller while the UISearchController is active.
This property will hold the candies that the user searches for.
isSearchBarEmpty returns true if the text typed in the search bar is empty;
otherwise, it returns false .
return candy.name.lowercased().contains(searchText.lowercased())
tableView.reloadData()
filter(_:) takes a closure of type (candy: Candy) -> Bool . It then loops over all
the elements of the array and calls the closure, passing in the current element, for every
one of the elements.
You can use this to determine whether a candy should be part of the search results that
the user receives. To do so, you need to return true if you want to include the current
candy in the filtered array or false otherwise.
6/16
To determine this, you use contains(_:) to see if the name of the candy contains
searchText . But before doing the comparison, you convert both strings to their
lowercase equivalents using lowercased() .
Note: Most of the time, users don’t bother with the case of letters when performing a
search, so by only comparing the lowercase version of what they type with the lowercase
version of the name of each candy, you can easily return a case-insensitive match. Now,
you can type “Chocolate” or “chocolate” and either will return a matching candy. How
useful is that?! :]
filterContentForSearchText(searchBar.text!)
Now, whenever the user adds or removes text in the search bar, the
UISearchController will inform the MasterViewController class of the change via
a call to updateSearchResults(for:) , which in turn calls
filterContentForSearchText(_:category:) .
Build and run and you’ll notice that there’s now a search bar above the table. You may
need to scroll down to see it.
7/16
However, when you enter search text, you still don’t see any filtered results. What gives?
This is simply because you haven’t written the code to let the table view know when to use
the filtered results yet.
8/16
func tableView(_ tableView: UITableView,
if isFiltering {
return filteredCandies.count
return candies.count
Not much has changed here. You simply check whether the user is searching or not, then
use either the filtered or the normal candies as the data source for the table.
if isFiltering {
candy = filteredCandies[indexPath.row]
} else {
candy = candies[indexPath.row]
cell.textLabel?.text = candy.name
cell.detailTextLabel?.text = candy.category.rawValue
return cell
Both methods now use isFiltering , which refers to the isActive property of
searchController to determine which array to display.
When the user taps the search field of the search bar, isActive is automatically set to
true . If the search controller is active and the user has typed something into the search
field, the returned data comes from filteredCandies . Otherwise, the data comes from
the full list of items.
Remember that the search controller automatically handles showing and hiding the
results table, so all your code has to do is provide the correct data (filtered or non-filtered)
depending on the state of the controller and whether the user has searched for anything.
Build and run the app. You now have a functioning Search Bar that filters the rows of the
main table. Huzzah!
9/16
Play with the app for a bit to see how you can search for various candies.
But wait, there’s still one more problem. When you select a row from the search results
list, you may notice the detail view is from the wrong candy! Time to fix that.
10/16
And replace it with the following:
if isFiltering {
candy = filteredCandies[indexPath.row]
} else {
candy = candies[indexPath.row]
Here, you perform the same isFiltering check as before, but now you’re providing the
proper candy object when segueing to the detail view controller.
Build and run the code at this point and see how the app now navigates correctly to the
detail view from either the main table or the search table with ease.
First, you have to create a scope bar in MasterViewController . The scope bar is a
segmented control that narrows a search by only looking in certain scopes. The scope is
whatever you define it to be. In this case, it’s a candy’s category, but scopes could also be
types, ranges or something completely different.
Using the scope bar is as easy as implementing one additional delegate method.
searchBar.scopeButtonTitles![selectedScope])
You call this delegate method when the user switches the scope in the scope bar. When
that happens, you want to redo the filtering. Thanks to RawRepresentable
conformance, you create a new category instance that retrieves the specified raw value
from the selected scope button title. So you call
filterContentForSearchText(_:category:) with the new category.
11/16
func filterContentForSearchText(_ searchText: String,
if isSearchBarEmpty {
return doesCategoryMatch
} else {
.contains(searchText.lowercased())
tableView.reloadData()
This now checks to see if the category of the candy matches the category that the scope
bar passes, or whether the scope is set to .all . You then check to see if there is text in
the search bar and filter the candy appropriately. Now, replace isFiltering with the
following:
let searchBarScopeIsFiltering =
searchController.searchBar.selectedScopeButtonIndex != 0
(!isSearchBarEmpty || searchBarScopeIsFiltering)
Here, you update isFiltering to return true when the user selects the scope bar.
You’re almost finished, but the scope filtering mechanism doesn’t quite work yet. You’ll
need to modify updateSearchResults(for:) in the first class extension you created to
send the current category:
searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex])
The only problem left is that the user doesn’t actually see a scope bar yet! Within
MasterViewController.swift in viewDidLoad() , add the following code just after the
search controller setup:
searchController.searchBar.scopeButtonTitles = Candy.Category.allCases
.map { $0.rawValue }
searchController.searchBar.delegate = self
12/16
Now, when you type, the selected scope button will appear in conjunction with the search
text.
Type in “caramel” with the scope set to “All”. It shows up in the list, but when you change
the scope to Chocolate, “caramel” disappears because it’s not a chocolate-type candy.
Hurrah!
There’s still one small problem with the app. You haven’t added a results indicator to tell
the user how many results they should expect to see. This is particularly important when
no results are returned at all, as it’s difficult for the user to distinguish between no results
returned and a delay in receiving the answer due to a slow network connection.
13/16
Adding a Results Indicator
To fix this, you’re going to add a footer to your view. The user will see this footer when
they filter the list of candies, and it will tell them how many candies are in the filtered
array.
Start by opening SearchFooter.swift. Here, you have a simple UIView which contains a
label as well as a public API that will receive the number of results returned.
notificationCenter.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
self.handleKeyboard(notification: notification)
notificationCenter.addObserver(
forName: UIResponder.keyboardWillHideNotification,
self.handleKeyboard(notification: notification)
These two observers allow you to control the results indicator, which will move up or
down based on the visibility of the system keyboard.
14/16
func handleKeyboard(notification: Notification) {
// 1
searchFooterBottomConstraint.constant = 0
view.layoutIfNeeded()
return
guard
else {
return
// 2
self.searchFooterBottomConstraint.constant = keyboardHeight
self.view.layoutIfNeeded()
})
1. You first check if the notification is has anything to do with hiding the keyboard. If
not, you move the search footer down and bail out.
2. If the notification identifies the ending frame rectangle of the keyboard, you move
the search footer just above the keyboard itself.
In both cases, you manage the distance between the search footer and the bottom of the
screen through a constraint represented by an IBOutlet called
searchFooterBottomConstraint .
Finally, you need to update the number of results in the search footer when the search
input changes. So replace tableView(_:numberOfRowsInSection:) with the following:
if isFiltering {
searchFooter.setIsFilteringToShow(filteredItemCount:
return filteredCandies.count
searchFooter.setNotFiltering()
return candies.count
Build and run, perform a few searches and watch as the footer updates.
15/16
Comments
16/16