Hands-On UISearchController: The complete guide

If you notice already, UISearchController is implemented on the Phone and Messages apps on the iOS system. This class was first introduced with the iOS 8 SDK, and is put to replace the deprecated UISearchDisplayController class.

UISearchController is designed to manage the presentation of a search bar and display search results together. You most likely want to use it when you need to search content within your app and dynamically filter results based on your search text.

In this tutorial, you gonna master the UISearchController set of API and tools while you implement a nice app to search over user informations pulled off from a server API.

Without further ado, let’s do it!

Open up Xcode, choose the Single View Application template and name the project “UsersDirectory”.

Create new Xcode project

Setting Up the Layout

The app you gonna build will grab some data from the server and display in a table view. It will also provide search capabilities and dynamic filtering within the table view.

So the final app consists of two screens. The first is where all the search work will be done, while the second will just display some details relevant to the selected row on the first screen.

Here is an illustration of what you gonna make 🙂

Final project demo

Select Main.storyboard file from the Project navigator view, then click on the ViewController from the canvas and choose Editor\Embed In\Navigation Controller from the menu.

You will notice a navigation bar is automatically added to the view controller, and the latter now belongs to a navigation controller stack.

Drag a UITableView object from the Object library to the view controller and make sure it fills all the screen below the navigation bar. Apply the relevant margins Auto Layout constraints to ensure the table view is always adjusted to the container borders, as shown below.

Add the view controller to the navigation controller

Add a UITableViewCell from the Object library to the table view. Select the added cell, then set its Identifier to “UserCell” from the Attributes inspector.

Next, add another UIViewController from the Object library to the scene, and perform the following steps:

1- Select the previously set ViewController, then ctrl click and drag a line to the newly added view controller. From the menu that shows up, select the “Show” segue.

2- Select the newly added storyboard segue, then set its identifier to “PushDetailsVC” from the Attributes inspector.

3- Add a UITableView from the Object library to the view controller and apply the same Auto Layout constraints so that it fills all the space under the navigation bar, as you did with the previous table.

4- Add a UITableViewCell from the Object library to the table view. Select the cell and set its Identifier to “DetailsCell” in the Attributes inspector.

Here is how the final storyboard canvas looks like:

Storyboad canvas

To finish up with the UI stuff, let’s create a new view controller class and assign it to the newly created view controller.

To do this, select “File\New\File…” from the menu, choose “iOS\Source\Cocoa Touch Class”, then name the class “DetailsViewController”. Make sure it’s a UIViewController subclass and save the file.

Switch back to the storyboard file, select the newly created view controller and set its class to “DetailsViewController” in the Identity inspector.

Before moving to code, let’s create two IBOutlet references for the table views in both controllers. To do this, perform the steps below:

1- Switch to the Assistant editor, select the View Controller from the canvas and make sure the ViewController.swift file is loaded along with the storyboard.

2- Ctrl click and drag a line from the table view to the class.

3- Make sure it’s an Outlet connection and name the reference “tableView”, then click the “Connect” button.

4- Repeat the above steps with the Details View Controller to make an Outlet reference for its table view. This time select the second view controller from the canvas and make sure the “DetailsViewController.swift” file is loaded in the split view of the Assistant editor. Call the outlet “tableView” as well.

So far so good, you are all set with the UI, now time for some code to bring this app to life 🙂

From the Project navigator view, select ViewController.swift file to load it in the editor.

First off, you gonna set up the search controller and put it as the header of the table view. Place the following properties declarations just after the class name:

// 1
var users:[[String:AnyObject]]!
var foundUsers:[[String:AnyObject]]!

// 2
var userDetails:[String:AnyObject]!
    
// 3
var resultsController: UITableViewController!
var searchController: UISearchController!

It’s important to know what each property is responsible for:

// 1: Here you declared two arrays of dictionaries, the users array will store the data source for the table view that will display the initial data, whereas the foundUsers array will handle all the filtered informations displayed by another table view while you search.

// 2: This will store the selected user informations in a dictionary to pass to the details view controller.

// 3: Here you set two references for the search controller and its relevant results controller. Remember, a results controller will manage the filtered data while you type in the search bar.

Next, implement the following code inside the viewDidLoad method (right after the super.viewDidLoad call):

// 1
users = []
foundUsers = []
self.tableView.dataSource = self
self.tableView.delegate = self        
self.automaticallyAdjustsScrollViewInsets = false
 
// 2              
resultsController = UITableViewController(style: .Plain)
resultsController.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "UserFoundCell")
resultsController.tableView.dataSource = self
resultsController.tableView.delegate = self
// 3
searchController = UISearchController(searchResultsController: resultsController)
searchController.hidesNavigationBarDuringPresentation = true
searchController.searchBar.searchBarStyle = .Minimal
searchController.searchResultsUpdater = self
self.tableView.tableHeaderView = searchController.searchBar
// 4
self.definesPresentationContext = true
// 5
let session = NSURLSession.sharedSession()
let url:NSURL! = NSURL(string: "http://jsonplaceholder.typicode.com/users")
let task = session.downloadTaskWithURL(url) { (location: NSURL?, response: NSURLResponse?, error: NSError?) -> Void in
   if (location != nil){
        let data:NSData! = NSData(contentsOfURL: location!)
        do{
            self.users = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves) as! [[String : AnyObject]]
            dispatch_async(dispatch_get_main_queue(), { () -> Void in
               self.tableView.reloadData()
            })
        }catch{
            // Catch any exception
            print("Something went wrong")
        }
   }else{
            // Error
            print("An error occurred \(error)")
        }
}
// Start the download task
task.resume()

Some points need explanations indeed:

// 1: Here you just initialised the data source arrays for both table views. Then you disabled the view controller from automatically adjusting the table view insets since you already manage that yourself earlier with Auto Layout.

// 2: This will create a results controller that will work along with the UISearchController to manage its filtered data. You defined the results controller as a UITableViewController instance since it will come with a table view object by default. You then registered a cell with an identifier for the table view and set the current view controller as the data source and delegate of the table view.

// 3: Here you initialised the search controller with the results controller previously set. You also set the current controller as the searchResultsUpdater delegate, so this class will implement the relevant protocol method to react to the search bar changes and dynamically filter data accordingly. Setting the search controller’s search bar as the table view header will place it always at the top of the list.

// 4: This is very important, the current view controller will present a search controller over its main view. Setting the definesPresentationContext property to true will indicate that the view controller’s view will be covered each time the search controller is shown over it. This will allow to avoid unknown behaviour.

// 5: This code will asynchronously request some set of data from the server and populate to the table view. This is done using a download task attached to a session object.

Next, change the class declaration to the following:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating{

Now the class conforms to the relevant protocols needed. You will now implement some protocol methods to ensure the class is effectively conforming to those protocols.

Let’s start with the UITableViewDataSource protocol, copy the following code before the class closing bracket:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if tableView == self.tableView{
      return users.count
    }
      return foundUsers.count
}
    
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
     var cellIdentifier: String!
     var dic: [String:AnyObject]!

     if tableView == self.tableView{
         cellIdentifier = "UserCell"
         dic = self.users[indexPath.row]
     }else{
         cellIdentifier = "UserFoundCell"
         dic = self.foundUsers[indexPath.row]
     }

     let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
     cell.textLabel?.text = dic["name"] as? String
     return cell
}

Nothing fancy from the code above, except that it always check which table view is calling the data source. The check is important since the current view controller is dealing with two table views.

Next, copy the following UISearchResultsUpdating protocol method implementation:

func updateSearchResultsForSearchController(searchController: UISearchController) {
     foundUsers.removeAll()
     for user in users{
         let userName:String! = user["name"] as? String
         if userName.localizedCaseInsensitiveContainsString(searchController.searchBar.text!) {
             foundUsers.append(user)
             self.resultsController.tableView.reloadData()
         }
    }
}

The above method will be called when the search bar becomes first responder (when the keyboard shows up) and each time a change is triggered inside the search bar. This seems to be the right place to dynamically filter the data based on the search text and reload the results controller’s table view with the filtered data. That’s typically what you did!

Note: You probably notice the updateSearchResultsForSearchController protocol method combines the same work that two of the UISearchBarDelegate protocol methods both do (the searchBarShouldBeginEditing and searchBar: textDidChange: methods).

So far, you can compile the app. But before running it, let’s first disable the ATS (Application Transport Security) in order to allow the app perform a server HTTP request.

Select the Info.plist file from the Project navigator view and make the following changes:

Disable App Transport Security

Cool, run the project and make sure the users informations are loaded in the table view. Try to filter the search to ensure the search results updater is working correctly 🙂

So far, you got a nice working dynamic filter search controller. Let’s finish up by implementing the relevant code to pass the selected user informations to the details controller.

Always in the ViewController.swift file, place the following code before the closing bracket of the class:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
     if tableView == resultsController.tableView{
         userDetails = foundUsers[indexPath.row]
         self.performSegueWithIdentifier("PushDetailsVC", sender: self)
     }
}
    
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    let detailsVC = segue.destinationViewController as! DetailsViewController
    detailsVC.userDetails = userDetails
}

Remember the segue you set up in the storyboard, the code above will use it to push to the details view controller. But before that, it will catch the selected user and store it to the details controller’s userDetails property.

From the Project navigator view, select DetailsViewController.swift file and make the following changes:

// Change the class declaration to the following
class DetailsViewController: UIViewController, UITableViewDataSource {

// Right after the class name
var userDetails:[String:AnyObject]!

// Inside the viewDidLoad method
self.automaticallyAdjustsScrollViewInsets = false
self.tableView.dataSource = self
// Before the class closing bracket
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
     return userDetails.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
     let cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier("DetailsCell")
        
     switch indexPath.row{
       case 0:
          cell.textLabel?.text = userDetails["name"] as? String
       case 1:
          cell.textLabel?.text = userDetails["username"] as? String
       case 2:
          cell.textLabel?.text = userDetails["email"] as? String
       case 3:
          cell.textLabel?.text = userDetails["website"] as? String
       default:
          break
       }
       return cell
}

That’s it, run the project and enjoy your search controller app 🙂

The final project is available to download here. Feel free to leave a comment below, I’d love to read your thoughts!

Malek
iOS developer with over than 11 years of extensive experience working on several projects with different sized startups and corporates.

11 Comments

  1. Really great post, thank you. By the way, can you please also share your PHP code as well.

  2. Hello, you said “You most likely want to use it when you need to search content within your app and dynamically filter results based on your search text.” I am trying to search from my database(Parse). I implemented UISearchController and had problems!! Do you think I should not use UISearchController??

  3. Hi Amal,
    Up to you. UISearchController is very useful for instant filtering data. What kind of problems did you face? I can help!

  4. Thank you so much!! I implemented UISearchController in my app to search for users to follow and unfollow. I am using swift and Parse as a backend. The problem is, whenever I type a space,”” , it returns all the data that are stored in Parse. In addition, when I try to delete what I just typed, every thing goes except the buttons,( I have name, image and button) on my cell. Im sorry if I am bothering you. When I solved one issue, another one just came out!! I really don’t know what is the problem. I really need someone to check my code. Thank you in advance for your help!

  5. Not working for me. Why everyone on internet assumes adding SearchBar programmaticaly? I have UISearchBar in UIViewController dragged in Xcode and cannot force it to work 🙁

  6. Hi 🙂
    I added it programmatically because I used the UISearchController class which comes with a built in search bar. UISearchController has no related objects in the Object library.

  7. I’m trying to have 2 search bars so that I can search for ‘to’ and ‘from’ addresses. I can implement one fine and it shows a tableview with the addresses matching my search, but how would you go about having 2 search bars within the same view? I have searched everywhere and can’t find any examples. I tried using a text field but I can’t call UISearchController(searchResultsController: resultsController) using a text field. Any suggestions on how to code a ‘to’ and ‘from’ address fields using a search?

Comments are closed.