How to Use UISearchController to Add Search Your UITableViews

This is a simple tutorial on how to use the new UISearchController to add search capability to your UITableViews. The UISearchController was introduced a couple years ago in iOS 8 to replace the now deprecated UISearchDisplayController. In the new search controller, it is easier to add search to your table views. As of Xcode 8.1, the UISearchController has not been added to Interface Builder, so you have to add it programmatically. Even still, it is pretty easy to work with.

Set Up our UITableView

In Interface Builder, add a UITableViewController to your storyboard. By default, the UITableViewController will use dynamic cells. That is perfect for our demo. The only other configuration we need to do is to configure our dynamic cell. Click on the cell and give it a reuse identifier. I labeled mine “basicCell.” Next, I set the cell style to “Basic,” because all I need is to display text in my cell.

uisearchcontroller_storyboard

Now we need to populate the table view with data, so we have something to search. I am currently watching Monday Night Football, so I have football on my mind. I am going to add all of the NFL teams to our table view. The following is pretty basic UITableView stuff. One function indicates how many sections we have, another indicates how many rows we have, and the final override configures the cell with data. They all use the data source unfilteredNFLTeams which is an array that I populate in the viewDidLoad function.


class MyTableViewController: UITableViewController {
    var unfilteredNFLTeams: [String]?
    
    override func viewDidLoad() {
        unfilteredNFLTeams = ["Bengals", "Ravens", "Browns", "Steelers", "Bears", "Lions", "Packers", "Vikings",
                              "Texans", "Colts", "Jaguars", "Titans", "Falcons", "Panthers", "Saints", "Buccaneers",
                              "Bills", "Dolphins", "Patriots", "Jets", "Cowboys", "Giants", "Eagles", "Redskins",
                              "Broncos", "Chiefs", "Raiders", "Chargers", "Cardinals", "Rams", "49ers", "Seahawks"].sorted()

        super.viewDidLoad()
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let nflTeams = unfilteredNFLTeams else {
            return 0
        }
        return nflTeams.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath)

        if let nflTeams = unfilteredNFLTeams {
            let team = nflTeams[indexPath.row]
            cell.textLabel!.text = team
        }

        return cell
    }
}

This is what our demo looks like when I run it.

uisearchcontroller_initial_list

Add the UISearchController

Now that our table view is setup with data to search, the next step is to add a UISearchController and configure it. The initializer to UISearchController can take an argument that can specify a different view controller to show the search results. I think the most common use case is to show the search results in the same view controller that has the table view. Passing nil indicates that we do not want another view controller to show the search results.


let searchController = UISearchController(searchResultsController: nil)

Now that we created the UISearchController, we need to apply a few settings. In the viewDidLoad function configure the UISearchController with the following code:


searchController.searchResultsUpdater = self
searchController.hidesNavigationBarDuringPresentation = false
searchController.dimsBackgroundDuringPresentation = false
tableView.tableHeaderView = searchController.searchBar

Let me explain what you just added:

  1. We need to implement the UISearchResultsUpdating Protocol (more on that later). Setting the searchResultsUpdater property to self, sets the delegate to our view controller instance.
  2. Setting the hidesNavigationBarDuringPresentation to false prevents the navigation bar from hiding while you type in the search bar.
  3. The dimsBackgroundDuringPresentation indicates whether the search results look dim when typing a search. I find this distracting, so I set it to false
  4. Last we set the header view of our table view to be the search bar of the UISearchController.

Implementing the UISearchResultsUpdating Protocol

This protocol only contains one function that we need to implement. The updateSearchResults function will get called for every character that is typed in the search bar. You need to filter your own data and update the table view yourself. Let’s do this for our NFL teams. This is my implementation of the updateSearchResults function.


func updateSearchResults(for searchController: UISearchController) {
    if let searchText = searchController.searchBar.text, !searchText.isEmpty {
        filteredNFLTeams = unfilteredNFLTeams.filter { team in
            return team.lowercased().contains(searchText.lowercased())
        }
        
    } else {
        filteredNFLTeams = unfilteredNFLTeams
    }
    tableView.reloadData()
}

I’ve introduced a second variable to my view controller to keep track of the filtered list of NFL teams called filteredNFLTeams. When someone types in the search bar, I need to update the filtered list with the NFL teams that contain those characters.

The if let statement checks if the searchBar.text is nil and also checks to see if the text is an empty string. If there is no search text, then I assign the original full list of NFL teams to the filteredNFLTeams variable. Otherwise I use Swift’s filter function on my list of NFL teams. I also lowercase each string to do a case insensitive search. Finally I tell the table view to reload it’s data.

Switch the table view to use the filtered dataset

The table view needs to use the filtered array of NFL teams as it’s data source instead of the original list. In the viewDidLoad function, I initialize the filteredNFLTeams array to be the same as the unfiltered list. Next, I swap out the use of the unfilteredNFLTeams variable for the filtered array in all of the table view data source functions. The only time we need the unfilteredNFLTeams array is when we filter the list based on the search bar text.


class MyTableViewController: UITableViewController, UISearchResultsUpdating {
    let unfilteredNFLTeams = ["Bengals", "Ravens", "Browns", "Steelers", "Bears", "Lions", "Packers", "Vikings",
                              "Texans", "Colts", "Jaguars", "Titans", "Falcons", "Panthers", "Saints", "Buccaneers",
                              "Bills", "Dolphins", "Patriots", "Jets", "Cowboys", "Giants", "Eagles", "Redskins",
                              "Broncos", "Chiefs", "Raiders", "Chargers", "Cardinals", "Rams", "49ers", "Seahawks"].sorted()
    var filteredNFLTeams: [String]?
    let searchController = UISearchController(searchResultsController: nil)
    
    override func viewDidLoad() {
        filteredNFLTeams = unfilteredNFLTeams
        searchController.searchResultsUpdater = self
        searchController.hidesNavigationBarDuringPresentation = false
        searchController.dimsBackgroundDuringPresentation = false
        tableView.tableHeaderView = searchController.searchBar
        
        super.viewDidLoad()
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let nflTeams = filteredNFLTeams else {
            return 0
        }
        return nflTeams.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath)
        
        if let nflTeams = filteredNFLTeams {
            let team = nflTeams[indexPath.row]
            cell.textLabel!.text = team
        }
        
        return cell
    }
    
    func updateSearchResults(for searchController: UISearchController) {
        if let searchText = searchController.searchBar.text, !searchText.isEmpty {
            filteredNFLTeams = unfilteredNFLTeams.filter { team in
                return team.lowercased().contains(searchText.lowercased())
            }
            
        } else {
            filteredNFLTeams = unfilteredNFLTeams
        }
        tableView.reloadData()
    }
}

When we run the app now, you can see the search bar appears at the top of the list. As I type, I can see my table view update.

uisearchcontroller_searching

Preventing the UISearchController from overstaying it’s welcome

If you have multiple scenes in your app, you will start to notice a problem that the UISearchController seems to hang around even when you navigate to other scenes in your app. It may be the correct behavior if you are using a second view controller to display your search results, but in my case, I want it to go away when I navigate away from my table view. One suggestion is to add the following code in the viewDidLoad function definesPresentationContext = true. I found this did not work for me. When I would navigate to the next scene and then go back to my NFL list, the list would be empty. Plus the search bar would still remain in some situations.

The better solution I found was to dismiss the UISearchController when I navigate away. I implemented the viewWillDisappear function as follows:


override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    searchController.dismiss(animated: false, completion: nil)
}

Hopefully you found that it was not too much work to add a UISearchController. Once Apple updates Interface Builder to include the UISearchController, it should be even less code.

One limitation with the UISearchController is it only works with UITableViewControllers. Regular UIViewControllers that contain a UITableView are not supported. In my next blog post, I’ll show you how you can get around this limitation.

Conversation
  • Sergei Hrushev says:

    While updating an old iOS 7 Objective-C project to iOS 10 I found that there’s a problem in UISearchController API: position of the “Cancel” button is incorrect and scope buttons aren’t displayed at all.

    And I spent some time to understand that with disabled “auto layout” it’s impossible to force it to work.

    So my comment is: to use UISearchController it’s necessary firstly to enable “auto layout”!

  • Dev Hard says:

    Very useful! Thank you

  • Jesus says:

    Hello Mike!
    I need some help using SearchController in a UIViewController. Im using a TableView and inside of mi TableView a SearchController. The problem is that when you select the search bar, i want to show a scope items (segmented control). But when this happens, the search bar moves outside of my header. And when dismiss the search bar, takes his place again. Im using ViewController since i want to show a banner of images and below of it, show my table view. But i cannot achieve it.

    Hope you can help me with some workaround.
    thanks, and congrats for this article, is very helpful.

  • Chris says:

    Mike thank you!

    I put together the search controller a while ago and found it easy to get going and use. However the ‘overstaying it’s welcome’ issue appeared and I was having all sorts trying to resolve this. I could not get viewWillDisappear to fire when the search controller was active and navigated away, I also tried similar dismissing code in all of the ‘view – will/did’ to no avail.

    The combination of PresentationContext and what you have here finally did the trick.

    Great blog that I keep returning to for information and interest. Thanks again.

  • sirman says:

    Thanks a let, very simple and usefull tutorial 👌

  • Kaan Esin says:

    Thanks a lot, it is very useful using UISearchController as you said for me too. Also it is easier than UISearchDisplayController and UISearchBar to integrate.

    Only thing that I don’t understand, you said “Regular UIViewControllers that contain a UITableView are not supported.” but when I tried my example (a tableview on a viewcontroller) in your blog, there is no problem for me.

    Also I have noticed a problem on UISearchController too. When search controller is presented on the screen, user selects an item of the list and for ex. if I want to present another screen, the application freezes and by clicking Cancel button app crashes. For this it could be also used dismissing search controller after clicking the row or etc.

  • Andrew says:

    Verry useful and straight to the point. I had sections in my tableView and this strategy mapped very easily to it. Thanks!!!

  • ivanko says:

    What about if I have two text labels ?
    how to search from two text labels ?

  • Comments are closed.