0% found this document useful (0 votes)
90 views16 pages

UISearchController Tutorial Getting Started

The document provides instructions for implementing a basic search feature using UISearchController in an iOS app. It describes: 1. Initializing a UISearchController property and setting it up with the view controller as the search results controller. 2. Making the view controller conform to the UISearchResultsUpdating protocol to respond to search text changes. 3. Adding a method to filter the candies array based on the search text, store the results in a new property, and reload the table view. 4. Calling this filtering method from within updateSearchResults(for:) to update the results on each search text change.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
90 views16 pages

UISearchController Tutorial Getting Started

The document provides instructions for implementing a basic search feature using UISearchController in an iOS app. It describes: 1. Initializing a UISearchController property and setting it up with the view controller as the search results controller. 2. Making the view controller conform to the UISearchResultsUpdating protocol to respond to search text changes. 3. Adding a method to filter the candies array based on the search text, store the results in a new property, and reload the table view. 4. Calling this filtering method from within updateSearchResults(for:) to update the results on each search text change.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 16

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.

Open Main.storyboard and look at the view controllers contained within:

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:

name: This property has type String and is fairly self-explanatory.


category: This is an enum of type Candy.Category , which represents the
category each candy belongs to. It also conforms to RawRepresentable so that you
can convert it to and from an associated raw value of type String .

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.

Populating the Table View


Open MasterViewController.swift. You’ll manage all the different Candy for your users
to search in candies . Speaking of which, it’s time to create some candy!

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.

In MasterViewController.swift, add a new property under candies ‘ declaration:

let searchController = UISearchController(searchResultsController: nil)

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.

In order for MasterViewController to respond to the search bar, it must implement


UISearchResultsUpdating . This protocol defines methods to update search results
based on information the user enters into the search bar.

Still in MasterViewController.swift, add the following class extension outside of the main
MasterViewController :

extension MasterViewController: UISearchResultsUpdating {

func updateSearchResults(for searchController: UISearchController) {

// 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.

Setting Up searchController ‘s Parameters

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

searchController.searchBar.placeholder = "Search Candies"

// 4

navigationItem.searchController = searchController

// 5

definesPresentationContext = true

Here’s a breakdown of what you’ve just added:

1. searchResultsUpdater is a property on UISearchController that conforms to


the new protocol, UISearchResultsUpdating . With this protocol,
UISearchResultsUpdating will inform your class of any text changes within the
UISearchBar .
2. By default, UISearchController obscures the view controller containing the
information you’re searching. This is useful if you’re using another view controller
for your searchResultsController . In this instance, you’ve set the current view
to show the results, so you don’t want to obscure your view.
3. Here, you set the placeholder to something that’s specific to this app.

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.

Filtering With UISearchResultsUpdating


After you set up the search controller, you’ll need to do some coding to get it working.
First, add the following property near the top of MasterViewController :

var filteredCandies: [Candy] = []

This property will hold the candies that the user searches for.

Next, add the following computed property to the main MasterViewController :

var isSearchBarEmpty: Bool {

return searchController.searchBar.text?.isEmpty ?? true

isSearchBarEmpty returns true if the text typed in the search bar is empty;
otherwise, it returns false .

Still within MasterViewController.swift, add the following method at the end of


MasterViewController :

func filterContentForSearchText(_ searchText: String,

category: Candy.Category? = nil) {

filteredCandies = candies.filter { (candy: Candy) -> Bool in

return candy.name.lowercased().contains(searchText.lowercased())

tableView.reloadData()

filterContentForSearchText(_:category:) filters candies based on


searchText and puts the results in filteredCandies , which you’ve just added. Don’t
worry about the category parameter for now; you’ll use that in a later section of this
tutorial.

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?! :]

Remember UISearchResultsUpdating ? You left it unimplemented. Well, you’ve just


written a method that you want to call when you update the search results. Voilà!

Replace the TODO in updateSearchResults(for:) with the following code:

let searchBar = searchController.searchBar

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.

Updating the Table View


In the main MasterViewController class of MasterViewController.swift, add a
computed property to determine if you are currently filtering results or not:

var isFiltering: Bool {

return searchController.isActive && !isSearchBarEmpty

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

8/16
func tableView(_ tableView: UITableView,

numberOfRowsInSection section: Int) -> Int {

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.

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

func tableView(_ tableView: UITableView,

cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

let candy: Candy

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.

Sending Data to a Detail View


When sending information to a detail view controller, you need to ensure the view
controller knows which context the user is working with: The full table list or the search
results. Here’s how you handle that.

Still in MasterViewController.swift, in prepare(for:sender:) , find the following


code:

let candy = candies[indexPath.row]

10/16
And replace it with the following:

let candy: Candy

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.

Creating a Scope Bar to Filter Results


To give your users another way to filter their results, add a scope bar in conjunction with
your search bar to filter items by category. The categories you’ll filter by are the ones you
assigned to the candy object when you created candies : Chocolate, Hard and Other.

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.

In MasterViewController.swift, you’ll add another extension that conforms to


UISearchBarDelegate . So after UISearchResultsUpdating , which you added
earlier, add the following:

extension MasterViewController: UISearchBarDelegate {

func searchBar(_ searchBar: UISearchBar,

selectedScopeButtonIndexDidChange selectedScope: Int) {

let category = Candy.Category(rawValue:

searchBar.scopeButtonTitles![selectedScope])

filterContentForSearchText(searchBar.text!, category: category)

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.

Now, modify filterContentForSearchText(_:category:) to take the supplied


category into account:

11/16
func filterContentForSearchText(_ searchText: String,

category: Candy.Category? = nil) {

filteredCandies = candies.filter { (candy: Candy) -> Bool in

let doesCategoryMatch = category == .all || candy.category == category

if isSearchBarEmpty {

return doesCategoryMatch
} else {

return doesCategoryMatch && candy.name.lowercased()

.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:

var isFiltering: Bool {

let searchBarScopeIsFiltering =

searchController.searchBar.selectedScopeButtonIndex != 0

return searchController.isActive &&

(!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:

func updateSearchResults(for searchController: UISearchController) {

let searchBar = searchController.searchBar

let category = Candy.Category(rawValue:

searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex])

filterContentForSearchText(searchBar.text!, category: category)

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

Since Candy.Category conforms to CaseIterable , the compiler can automatically


synthesize allCases for any RawRepresentable enumeration, adding the titles that
match the categories you assigned to your candy objects.

12/16
Now, when you type, the selected scope button will appear in conjunction with the search
text.

Testing the Scope Bar


Build and run. Try entering some search text and changing the scope.

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.

Head back to MasterViewController.swift. searchFooter is an IBOutlet for the


search footer that the starter project already set up for you. You can find it in the master
scene in Main.storyboard, at the bottom of the screen.

Within MasterViewController.swift, add the following to viewDidLoad() , after the spot


where you set up the scope bar:

let notificationCenter = NotificationCenter.default

notificationCenter.addObserver(

forName: UIResponder.keyboardWillChangeFrameNotification,

object: nil, queue: .main) { (notification) in

self.handleKeyboard(notification: notification)

notificationCenter.addObserver(

forName: UIResponder.keyboardWillHideNotification,

object: nil, queue: .main) { (notification) in

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.

Next, add this method to MasterViewController :

14/16
func handleKeyboard(notification: Notification) {

// 1

guard notification.name == UIResponder.keyboardWillChangeFrameNotification else


{

searchFooterBottomConstraint.constant = 0

view.layoutIfNeeded()

return

guard

let info = notification.userInfo,

let keyboardFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue

else {

return

// 2

let keyboardHeight = keyboardFrame.cgRectValue.size.height

UIView.animate(withDuration: 0.1, animations: { () -> Void in

self.searchFooterBottomConstraint.constant = keyboardHeight

self.view.layoutIfNeeded()
})

Here’s what’s happening with the code you just added:

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:

func tableView(_ tableView: UITableView,

numberOfRowsInSection section: Int) -> Int {

if isFiltering {

searchFooter.setIsFilteringToShow(filteredItemCount:

filteredCandies.count, of: candies.count)

return filteredCandies.count

searchFooter.setNotFiltering()

return candies.count

All you’ve done here is to add in calls to the searchFooter .

Build and run, perform a few searches and watch as the footer updates.

15/16
Comments

16/16

You might also like