Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

UISearchController Tutorial: Getting Started

$
0
0
UISearchBar-square

So much data, so little time.

Note: This tutorial has been updated to Xcode 9 beta, Swift 4, and iOS 11 by Tom Elliott. The original tutorial was written by Andy Pereira.

Scrolling through massive lists of items can be a slow and frustrating process for users. When dealing with large datasets, it’s vitally important to let the user search for specific items. UIKit includes UISearchBar, which seamlessly integrates with UITableView and allows for quick, responsive filtering of information.

In this UISearchController tutorial, you’ll build a searchable Candy app based on a standard table view. You’ll add table view search capability and dynamic filtering, and add an optional scope bar, all while taking advantage of UISearchController. In the end, you’ll know how to make your apps much more user friendly and satisfy your users’ urgent demands.

Ready for some sugar-coated search results? Read on.

Getting Started

Download the starter project from the tutorial here and open the project. The app has already been set up with a navigation controller. In the Xcode project navigator, select the project file, CandySearch, then select the target CandySearch, and in the signing section update the team to your own development team. Build and run the app, and you’ll see an empty list:

UISearchController Tutorial - Starter

Back in Xcode, the file Candy.swift contains a struct to store the information about each piece of candy you’ll be displaying. This struct has two properties: the category and name of the candy.

When the user searches for a candy in your app, you’ll be searching the name property using the user’s query string. You’ll see how the category string will become important near the end of this UISearchController tutorial when you implement the Scope Bar.

Populating the Table View

Open MasterViewController.swift. The candies property will be where you manage all the different Candy objects for your users to search. Speaking of which, it’s time to create some candy!

In this UISearchController 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 used will remain the same. Scalability at its finest!

To populate your candies array, add the following code to viewDidLoad(), after the call to super.viewDidLoad():

candies = [
  Candy(category:"Chocolate", name:"Chocolate Bar"),
  Candy(category:"Chocolate", name:"Chocolate Chip"),
  Candy(category:"Chocolate", name:"Dark Chocolate"),
  Candy(category:"Hard", name:"Lollipop"),
  Candy(category:"Hard", name:"Candy Cane"),
  Candy(category:"Hard", name:"Jaw Breaker"),
  Candy(category:"Other", name:"Caramel"),
  Candy(category:"Other", name:"Sour Chew"),
  Candy(category:"Other", name:"Gummi Bear"),
  Candy(category:"Other", name:"Candy Floss"),
  Candy(category:"Chocolate", name:"Chocolate Coin"),
  Candy(category:"Chocolate", name:"Chocolate Egg"),
  Candy(category:"Other", name:"Jelly Beans"),
  Candy(category:"Other", name:"Liquorice"),
  Candy(category:"Hard", name:"Toffee Apple")
]

Build and run your project. Since the table view’s delegate and dataSource methods have already been implemented, you’ll see that you now have a working table view:

UISearchController Tutorial-Data

Selecting a row in the table will also display a detail view of the corresponding candy:

Dark Chocolate

So much candy, so little time to find what you want! You need a UISearchBar.

Introducing UISearchController

If you look at the UISearchController documentation, you’ll discover it’s pretty lazy. It doesn’t do any of the work of searching at all. The class simply provides a 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 a tad scary at first, writing custom search functions gives you tight control over how results are returned specifically in your app. Your users will appreciate searches that are intelligent — and fast.

If you’ve worked with searching table views iOS in the past, you may be familiar with UISearchDisplayController. Since iOS 8, this class has been deprecated in favor of UISearchController, which simplifies the entire search process.

Unfortunately, Interface Builder does not support UISearchController at the time of this writing, so you’ll have to create your UI programmatically.

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

let searchController = UISearchController(searchResultsController: nil)

By initializing UISearchController with a nil value for the searchResultsController, you tell the search controller that you want use the same view you’re searching to display the results. If you specify a different view controller here, that will be used to display the results instead.

To allow MasterViewController to respond to the search bar, it will have to 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 class:

extension MasterViewController: UISearchResultsUpdating {
  // MARK: - UISearchResultsUpdating Delegate
  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.

Next, you’ll need to set up a few parameters for your searchController. Still in MasterViewController.swift, add the following to viewDidLoad(), after the call to super.viewDidLoad():

  // Setup the Search Controller
  searchController.searchResultsUpdater = self
  searchController.dimsBackgroundDuringPresentation = false
  definesPresentationContext = true
  tableView.tableHeaderView = searchController.searchBar

Here’s a rundown of what you just added:

  1. searchResultsUpdater is a property on UISearchController that conforms to the new protocol UISearchResultsUpdating. This protocol allows your class to be informed as text changes within the UISearchBar.
  2. By default, UISearchController will dim the view it is presented over. This is useful if you are using another view controller for searchResultsController. In this instance, you have set the current view to show the results, so you do not want to dim your view.
  3. By setting definesPresentationContext on your view controller to true, you ensure that the search bar does not remain on the screen if the user navigates to another view controller while the UISearchController is active.
  4. Finally, you add the searchBar to your table view’s tableHeaderView. This is necessary as Interface Builder is not yet compatible with UISearchController.

UISearchResultsUpdating and Filtering

After setting 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 is searching for.

Next, add the following helper methods to the main MasterViewController class:

// MARK: - Private instance methods

func searchBarIsEmpty() -> Bool {
  // Returns true if the text is empty or nil
  return searchController.searchBar.text?.isEmpty ?? true
}

func filterContentForSearchText(_ searchText: String, scope: String = "All") {
  filteredCandies = candies.filter({( candy : Candy) -> Bool in
    return candy.name.lowercased().contains(searchText.lowercased())
  })

  tableView.reloadData()
}

searchBarIsEmpty() is fairly self-explanatory. filterContentForSearchText(_:scope:) filters the candies array based on searchText and will put the results in the filteredCandies array you just added. Don’t worry about the scope 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 presented to the user. To do so, you need to return true if the current candy is to be included in the filtered array, or false otherwise.

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 the lowercased() method.

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 a TODO in updateSearchResults(for:). Well, you’ve now just written a method that should be called when we want to update the search results. Voilà!

Replace the TODO in updateSearchResults(for:) with a call to filterContentForSearchText(_:scope:):

filterContentForSearchText(searchController.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(_:scope:).

Build and run the app now, and you’ll notice that there is now a Search Bar above the table.

UISearchController Tutorial - Showing the Search Bar

However, if you enter some search text you still don’t see any filtered results. What gives? This is simply because you haven’t yet written the code to let the table view know when to use the filtered results.

Updating the Table View

Back in MasterViewController.swift, add a method to determine if you are currently filtering results or not:

func isFiltering() -> Bool {
  return searchController.isActive && !searchBarIsEmpty()
}

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

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, and use either the filtered or normal candies as a 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
  return cell
}

Both methods now use isFiltering(), which refers to the active property of searchController to determine which array to display.

When the user clicks in the search field of the Search Bar, active will automatically be set to true. If the search controller is active, and the user has actually typed something into the search field, the data returned is taken from the filteredCandies array. Otherwise, the data comes from the full list of items.

Recall 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’ve got a functioning Search Bar that filters the rows of the main table. Huzzah!

UISearchController Tutorial -Filter

Play with the app for a bit to see how you can search for various candies.

Changing the Search Bar Appearance

This works great, but have you noticed the color of the search bar? It uses the default iOS colors. And while those are lovely, they don’t really scream “candy”, do they? :]

In AppDelegate.swift, add the following to the bottom of application(_:didFinishLaunchingWithOptions:), just before the return statement:

UISearchBar.appearance().barTintColor = .candyGreen
UISearchBar.appearance().tintColor = .white
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = .candyGreen

Here, you use the UIAppearance API to set the bar tint color of your search bar to candy green, its tint color to white and then finally the tint color of any UITextField elements contained within a UISearchBar also to candy green.

Build and run the app, and perform another search.

Search Bar in the correct color

Notice how your search bar is now green, and the cancel button is white. Isn’t it beautiful? :]

There’s still one more problem. When you select a row from the search results list, you may notice that the detail view shown is of 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 that the view controller knows which context the user is working with: the full table list, or the search results.

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

let candy = candies[indexPath.row]

Then replace it with the following:

let candy: Candy
if isFiltering() {
  candy = filteredCandies[indexPath.row]
} else {
  candy = candies[indexPath.row]
}

Here you performed the same isFiltering() check as before, but now you’re providing the proper candy object when performing a segue 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.

CandySearch-DetailView

Creating a Scope Bar to Filter Results

If you wish to give your users another way to filter their results, you can add a Scope Bar in conjunction with your search bar in order to filter items by category. The categories you will filter on are the ones you assigned to the Candy object when you created the candies array: Chocolate, Hard, and Other.

First, you have to create a scope bar in MasterViewController. The scope bar is a segmented control that narrows down a search by only searching in certain scopes. A scope is really what you define it as. 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 need to add another extension that conforms to UISearchBarDelegate. After the UISearchResultsUpdating extension which you added earlier, add the following:

extension MasterViewController: UISearchBarDelegate {
  // MARK: - UISearchBar Delegate
  func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
    filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
  }
}

This delegate method is called when the user switches the scope in the scope bar. When that happens, you want to redo the filtering, so you call filterContentForSearchText(_:scope:) with the new scope.

Now modify filterContentForSearchText(_:scope:) to take the supplied scope into account:

func filterContentForSearchText(_ searchText: String, scope: String = "All") {
  filteredCandies = candies.filter({( candy : Candy) -> Bool in
    let doesCategoryMatch = (scope == "All") || (candy.category == scope)

    if searchBarIsEmpty() {
      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 passed in via the scope bar (or if the scope is set to “All”). You then check to see if there is text in the search bar, and filter the candy appropriately. You also need to update isFiltering() to return true when the scope bar is selected.

func isFiltering() -> Bool {
  let searchBarScopeIsFiltering = searchController.searchBar.selectedScopeButtonIndex != 0
  return searchController.isActive && (!searchBarIsEmpty() || searchBarScopeIsFiltering)
}

You’re almost there, 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 currently selected scope:

func updateSearchResults(for searchController: UISearchController) {
  let searchBar = searchController.searchBar
  let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
  filterContentForSearchText(searchController.searchBar.text!, scope: scope)
}

The only problem left is that the user doesn’t actually see a scope bar yet! To add the scope bar, move to the spot in the code just after the search controller setup. In viewDidLoad() within MasterViewController.swift, add the following code just before the assignment to candies:

// Setup the Scope Bar
searchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"]
searchController.searchBar.delegate = self

This will add a scope bar to the search bar, with the titles that match the categories you assigned to your candy objects. You also include a catch-all category called “All” that you will use to ignore the candy category in the search altogether.

Now, when you type, the selected scope button will be used in conjunction with the search text.

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

UISearchController Tutorial -Filter with 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. Hurrah!

Note: At the time of writing, if you’re running this tutorial with Swift 4 you will notice that the top row in the table view is hidden behind the scope bar. This doesn’t happen with earlier versions of iOS and we believe this is a bug in the Beta of iOS 11. We have filed a radar with Apple, and you can keep track of its progress here.

There is still one small problem with the app. We don’t indicate to the user how many results they should expect to see. This is particularly important when there are no results returned at all, as it can be difficult for the user to distinguish between something such as no results returned and a slow network connection fetching the answer.

Adding a Results Indicator

To fix this, we’re going to add a footer to our view. This will be visible when filtering is applied to our list of candies and will tell the user how many candies are in the filtered array. Open SearchFooter.swift. Here you have a simple UIView which contains a label and has a public API to be notified about the number of results returned.

Head back to MasterViewController.swift. At the top of the class you already set up an IBOutlet for the search footer, which can be seen in the master scene in Main.storyboard at the bottom of the screen. Add the following to viewDidLoad(), after the spot where you set up the scope bar:

// Setup the search footer
tableView.tableFooterView = searchFooter

This sets your custom search footer view as the table view’s footer view. Next, you need to update it on the number of results when the search input changes. 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 add in calls to the searchFooter.

Build and run the application, perform a few searches and watch the footer update as the searches return. You’ll need to hit the search button to dismiss the keyboard and see the search footer.

UISearchController-Filter with footer

UISearchController-Filter with footer, no results

Where To Go From Here?

Congratulations – you now have a working app that allows you to search directly from the main table view.

Here is a downloadable sample project with all of the code from this UISearchController tutorial.

Table views are used in all kinds of apps, and offering a search option is a nice touch for usability. With UISearchBar and the UISearchController, iOS provides much of the functionality out of the box so there’s no excuse for not using it. The ability to search a large table view is something that today’s users expect; when they find it isn’t present, they won’t be happy campers.

I hope to see you adding search capabilities to your table view apps in the future. If you have any questions or comments, please join the forum discussion below!

The post UISearchController Tutorial: Getting Started appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>