Update note: This tutorial was updated to iOS 8 and Swift by Brad Johnson. Original post by Tutorial Team Member Nicolas Martin.
In the mobile app world, people want their information fast, and they want it now!
iOS users expect their data to be available on-demand and presented quickly. Furthermore, they expect this to all happen in an easy to use and intuitive manner. It’s a tall order, to be sure!
Many UIKit based applications use the UITableView as a way to navigate through data, since scrolling is natural and fast. But what about the cases where there are large, even huge, amounts of data to sift through? With large datasets, scrolling through massive lists becomes slow and frustrating – so it’s vitally important to allow users to search for specific items. Lucky for us, UIKit includes UISearchBar which seamlessly integrates table view search and allows for quick, responsive filtering of information.
In this tutorial, you’ll build a searchable Candy app which is based on a standard table view. You’ll add table view search capability, including dynamic filtering and adding an optional Scope Bar. 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!
Note: At the time of writing this tutorial, our understanding is we cannot post screenshots of beta software. All the screenshots here are from iOS 7 and we are suppressing Xcode 6 screenshots until we are sure it is OK.
Getting Started
In Xcode, create a new project by going to File\New\Project…. Choose Single View Application and click Next. Name the project CandySearch and make sure that Language is set to Swift and Devices is set to iPhone. Click Finish, browse to the location you want to create the project and then click Create.
Start by clearing out some of the default files so you can truly start from scratch. In the Project Navigator, select ViewController.swift, right-click, select Delete, and then click Move to Trash. Then open up Main.storyboard, select the only view controller and delete it. Now that you have an empty storyboard to work with, you can add the main screens in your application.
From the Object Browser (the lower half of the right sidebar) drag out a Navigation Controller to add iOS’s built-in navigation logic to the project. This will create two view controllers on the storyboard – the navigation controller itself and a table view controller inside that will be the initial view for the application.
You’ll need one more view controller as the detail view controller that is displayed when the user selects an item from the list. Drag a View Controller object onto the storyboard. Control-drag from the Table View Controller to the new view controller and select show as the manual segue from the popup.
Setting Up the Candy Class
Next you will create a struct to model the information about each piece of candy you’re displaying, such as its category and name. To do this, right click on the CandySearch folder and select New File…. Select iOS \ Source \ Swift File and click Next. Name the file Candy.swift. Open the file, and add the following definition:
struct Candy { let category : String let name : String } |
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 referencing the name property against the user’s search string. You’ll see how the category string will become important near the end of this tutorial when you implement the Scope Bar.
You don’t need to add your own initializer here since you’ll get an automatically-generated one. By default, the initializer’s parameters will match up with properties you define in the same order. You’ll see how to create candy
instances in the next section.
Now you are ready to set up the UITableView
that your UISearchBar
will filter!
Connecting the Table View
Next you will set up a UITableView
that will work with the UISearchBar
. Create a new file by right clicking on the CandySearch folder and selecting New File…. Select iOS \ Source \ Cocoa Touch Class and click Next. Name the class CandyTableViewController, make it a subclass of UITableViewController and set the Language to Swift.
You will start by adding an array for the sample data. Open CandyTableViewController.swift and add the following code inside the class definition:
var candies = [Candy]() |
The candies
array 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 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 it’s finest! To populate your candies
array, override the viewDidLoad
as follows:
override func viewDidLoad() { super.viewDidLoad() // Sample Data for candyArray self.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")] // Reload the table self.tableView.reloadData() } |
This code doesn’t do much yet, but it does some important set up for later. First, it populates the candies
array with 9 different candies and categories. You’ll use this array later when you populate the table. Second, you tell the tableView
that it should reload it’s data. You need to do this to ensure all the code you are about to write gets called after the candies
array is populated.
Next, you’ll add functions that govern the table view itself. Implement the tableView(_:numberOfRowsInSection:)
like so:
override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { return self.candies.count } |
This simply tells the tableView
that it should contain as many rows as there are items in the candies
array you populated earlier.
Now that the tableView
knows how many rows to use, you need to tell it what to put in each row. You’ll do this by implementing tableView(_:cellForRowAtIndexPath:)
:
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! { //ask for a reusable cell from the tableview, the tableview will create a new one if it doesn't have any let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell // Get the corresponding candy from our candies array let candy = self.candies[indexPath.row] // Configure the cell cell.textLabel.text = candy.name cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator return cell } |
This function has three main steps. First, you dequeue a cell to use for this indexPath
. You then access the candies
array, reference the indexPath
to decide which Candy
object to pull, and then use that Candy
object to populate the UITableViewCell
.
heightForRowAtIndexPath
. As of iOS 8, this method is no longer required; the OS will determine the size of your cells at runtime to determine the height. Wasn’t that nice of them? :] Of course, if your app is going to support an earlier version of iOS, you will need to implement this method. You’re almost there! There’s one last step you’ll need to take before you can see your list of candies. You need to hook up all your code to your storyboard in Interface Builder. First, open Main.storyboard and select the Root View Controller. In the Identity Inspector (third tab on the top half of the right sidebar), set the Class to CandyTableViewController. This will tell the OS to load your custom class when this controller is pushed on the screen. While you’re at it, double click the Root View Controller title and change it to CandySearch to give your users an idea of what they’re doing.
Finally, you need to tell the OS what cell to use when your code looks for a new one. You may have noticed when you created the storyboard earlier that a default cell was placed in the table view. Select the cell (conveniently named “Cell”) and open the Attributes Inspector (just to the right of the Identity Inspector). Change the Identifier to Cell. This matches the reuse identifier you used in your code earlier to dequeue the cell.
Save your changes and build and run. You now have a working table view! So much candy…so little time! We need…a UISearchBar!
Setting Up the UISearchBar
If you look at the UISearchBar 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. It’s more like a middle-class manager in that respect; it’s great at delegating tasks to others.
The UISearchBar class communicates with a delegate protocol to let the rest of your app know what the user is doing. All of the actual functions for string matching and other operations will be written by you. Although this may seem a tad scary at first (and more than a little unfair!), 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.
Start by opening up Main.storyboard and dragging a Search Bar and Search Display Controller object to the table view controller. Be careful — this is different from the Search Bar object, which is also available. Position the Search Bar between the Navigation bar and the Table View.
Not sure what’s meant by a search display controller? According to Apple’s own documentation, a search display controller “manages display of a search bar and a table view that displays the results of a search of data managed by another view controller.”
That means the search display controller will have its own table view to display the results, separate from the one you just set up. So basically, the search display controller added above handles the task of showing the filtered data from a search in a separate view controller that you don’t have to worry about. :]
UISearchBar Options in the Attributes inspector
While in the storyboard, take a moment to review the properties available for the Search Bar object. You may not use all of them, but knowing what’s available in the Attributes Inspector is always valuable when working with a new UIKit component.
- Text: This will change the actual string value that is present in the search bar. As your app has no need to have a default value in the search bar, you won’t need this.
- Placeholder: This does exactly what you might expect – it allows you to put the light gray text in the Search bar that tells the user what the search bar can be used for. In the case of the Candy app, use “Search for Candy”.
- Prompt: This text will appear directly above the search bar. This is good for apps that have complicated search mechanisms, where the user might need instructions. (But in this app, the Placeholder text should be pretty clear!)
- Search Style, Bar Style, Translucent, Tint, Background and Scope Bar Images: These options allow you to customize the appearance of your search bar. The options are almost identical to those of the UINavigationBar and it is normally advisable to have your UISearchBar match your UINavigationBar for harmonious design.
- Search Text and Background Positions: These options allow you to add a custom offset to both your search text and background image.
- Show Search Results Button: Provides a button on the right side of the search bar for performing functions such as displaying recent searches or showing the last search results. Interaction with this button is managed through the Search Bar Delegate methods.
- Show Bookmarks Button: Shows the standard blue oval bookmarks icon in the right hand side of the search bar. Users expect this to bring up a list of their saved bookmarks. Like the search results button, this is also managed through the Search Bar Delegate methods.
- Show Cancel Button: This button allows users to close the separate view controller that is generated by the search bar. Leave this unchecked, as the search bar will automatically show and hide the Cancel button when the user is in Search mode.
- Shows Scope Bar & Scope Titles: The scope bar allows users to refine their search by limiting the results to a certain category, or scope. In a music application, for example, this bar may show choices such as Artists, Albums or Genres. For now, leave this box unchecked; you will implement your own scope bar later in this tutorial.
- Capitalize, Correction, Keyboard, etc.: These are options that have been borrowed from the normal UITextField options and allow you to change your search bar’s behavior. For example, if the user will be searching on proper nouns like businesses or last names, you may want to consider turning off correction as it will be annoying to users. In this tutorial, the candy names are all common nouns, so the default options will suffice.
UISearchBarDelegate and Filtering
After setting up the storyboard, you’ll need to do some coding work to get the search bar working. To allow the CandyTableViewController
class to respond to the search bar, it will have to implement a few protocols. Open CandyTableViewController.swift and replace the class declaration with the following:
class CandyTableViewController : UITableViewController, UISearchBarDelegate, UISearchDisplayDelegate { |
UISearchBarDelegate
defines the behavior and response of a search, while UISearchDisplayDelegate
defines the look and feel of the search bar.
Next, add the following property to the class:
var filteredCandies = [Candy]() |
This array will hold the filtered search results.
Next, add the following helper method to the class:
func filterContentForSearchText(searchText: String) { // Filter the array using the filter method self.filteredCandies = self.candies.filter({( candy: Candy) -> Bool in let categoryMatch = (scope == "All") || (candy.category == scope) let stringMatch = candy.name.rangeOfString(searchText) return categoryMatch && (stringMatch != nil) }) } |
This method will filter the candies
array based on searchText
(which is the search string entered by the user), and will put the results in the filteredCandies
array. Swift Arrays have a method called filter() that takes a closure expression as its only parameter.
If you have some experience with Objective-C, think of closure expressions as Swift’s version of blocks. A closure is a self contained block of functionality. They are called closures because they can capture and store references to any variables or constants that are created or used in the same context the closure is created, which is known as closing. Here is the syntax for a closure expression:
{(parameters) -> (return type) in expression statements} |
In this case, the filter method uses the closure expression separately on each element of the array being filtered. The parameter of the closure expression is the individual element being sorted. The closure expression returns a Bool
that is true if the element should be included in the new filtered array, or false if the element should not be included. If you look at the documentation of this method, you’ll notice it uses a type referred to as <T>
. This is a generic type, which means it can be any type. Since you are defining you’re own filter rules inside the closure expression, this filter method will work on an array filled with any kind of type. The closure expression’s parameter is also of type T
, which is the element being filtered. In your code you replaced that with candy: Candy
, since you know this array is filled with Candy
objects.
rangeOfString()
checks if a string contains another string. If it does and the category also matches, you return true and the current candy is included in the filtered array; if it doesn’t you return false and the current candy is not included.
Next, add the following two methods to the class:
func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool { self.filterContentForSearchText(searchString) return true } func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchScope searchOption: Int) -> Bool { self.filterContentForSearchText(self.searchDisplayController.searchBar.text) return true } |
These two methods are part of the UISearchDisplayControllerDelegate
protocol. They will call the content filtering function when the the user enters a search query. The first method runs the text filtering function whenever the user changes the search string in the search bar. The second method will handle the changes in the Scope Bar input. You haven’t yet added the Scope Bar in this tutorial, but you might as well add this UISearchBarDelegate
method now since you’re going to need it later.
Build and run the app now; you’ll notice that using the Search Bar still does not bring up any filtered results! What gives? This is simply because you haven’t yet written the code to let the tableView
know when to use the normal data vs. the filtered data. You’ll need to modify both the numberOfRowsInSection
and cellForRowAtIndexPath
functions. Replace the functions with the new implementations below:
override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { if tableView == self.searchDisplayController.searchResultsTableView { return self.filteredCandies.count } else { return self.candies.count } } override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! { //ask for a reusable cell from the tableview, the tableview will create a new one if it doesn't have any let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell var candy : Candy // Check to see whether the normal table or search results table is being displayed and set the Candy object from the appropriate array if tableView == self.searchDisplayController.searchResultsTableView { candy = filteredCandies[indexPath.row] } else { candy = candies[indexPath.row] } // Configure the cell cell.textLabel.text = candy.name cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator return cell } |
This code tests to see if the currently displayed tableView
is the search table or the normal table. If it is indeed the search table, the data is taken from the filteredCandies
array. Otherwise, the data comes from the full list of items. Recall that the search display 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 which table view is currently displaying.
Build and run the app. You’ve got a functioning Search Bar that filters the rows of the main table! Huzzah! Play with the app for a bit to see how the user can search for various candies.
You’ve probably noticed that that the if/else logic found in the tableView(numberOfRowsInSection:)
method is reused quite a few times. This is important when working with the Search Bar Display Controller and omitting this if/else check may result in bugs that will be difficult to track down. Just remember that the filtered results do not appear in the same table view as the main table. They are actually completely separate table views, but Apple has designed them in such a way that the experience is seamless for the end user — at the expense of being confusing to the novice developer!
Sending Data to a Detail View
When sending information to a detail view controller, you need to ensure that the view controller knows which table view the user is working with: the full table list, or the search results. The code for this will be similar to the code that you wrote in tableView(_:numberOfRowsInSection:)
and tableView(_:cellForRowAtIndexPath:)
. Still in CandyTableViewController.swift, add the following methods to the class:
override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) { self.performSegueWithIdentifier("candyDetail", sender: tableView) } override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) { if segue.identifier == "candyDetail" { let candyDetailViewController = segue.destinationViewController as UIViewController if sender as UITableView == self.searchDisplayController.searchResultsTableView { let indexPath = self.searchDisplayController.searchResultsTableView.indexPathForSelectedRow() let destinationTitle = self.filteredCandies[indexPath.row].name candyDetailViewController.title = destinationTitle } else { let indexPath = self.tableView.indexPathForSelectedRow() let destinationTitle = self.candies[indexPath.row].name candyDetailViewController.title = destinationTitle } } } |
Open up the storyboard and make sure that the segue from the Candy Table View Controller to the Detail View has the identifier candyDetail. Build and run the code at this point and see how the app now navigates to the Detail View from either the main table or the search table with ease.
Creating an Optional 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 out items by their category. The categories you will filter on are the ones you assigned to the Candy
object when the candyArray
was created: chocolate, hard, and other.
First, set up the scope bar on the storyboard. Go to the CandySearch View Controller and select the search bar. In the attributes inspector, check Shows Scope Bar in the Options section. Then modify the Scope Titles to be: “All”, “Chocolate”, “Hard” ,and “Other”. (You can use the + button to add more scope items and if you double-click an item, you can edit the item title).
Next, modify filterContentForSearchText
in CandyTableViewController.swift to take the new scope into account. Replace the current method implementation with the following:
func filterContentForSearchText(searchText: String, scope: String = "All") { self.filteredCandies = self.candies.filter({( candy : Candy) -> Bool in var categoryMatch = (scope == "All") || (candy.category == scope) var stringMatch = candy.name.rangeOfString(searchText) return categoryMatch && (stringMatch != nil) }) } |
This method now takes in an option scope
variable (it has a default value of “All” if nothing is passed in). The method now checks both the scope and the string, and only returns true if both the category and the string match (or the category is set to “All”).
Now that you’ve modified this method, you’ll need to change the two searchDisplayController
methods to account for the scope:
func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool { let scopes = self.searchDisplayController.searchBar.scopeButtonTitles as [String] let selectedScope = scopes[self.searchDisplayController.searchBar.selectedScopeButtonIndex] as String self.filterContentForSearchText(searchString, scope: selectedScope) return true } func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchScope searchOption: Int) -> Bool { let scope = self.searchDisplayController.searchBar.scopeButtonTitles as [String] self.filterContentForSearchText(self.searchDisplayController.searchBar.text, scope: scope[searchOption]) return true } |
Both methods now pull the scope out of the search bar and pass it to the new filter method. Build and run now. Here’s how your scope bar should look:
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 sample project with all of the code from the above 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 UISearchDisplayController
, 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!
How to Add Table View Search in Swift is a post from: Ray Wenderlich
The post How to Add Table View Search in Swift appeared first on Ray Wenderlich.