iOS UISearchController with UITableView

Filed Under: iOS

In this tutorial, we’ll be developing an application that implements a search function over a TableView using UISearchController. We’ve done something similar in an Android Tutorial here.

UISearchController

UISearchController contains a protocol UISearchResultsUpdating which informs our ViewController class through the function given below whenever text in the UISearchBar is changed.

func updateSearchResults(for searchController: UISearchController)

In the following application, we’ll use a TableViewController as the default view controller type and add a UISearchController at the top of it programmatically(UISearchController is not available in the Interface Builder). We’ll be using a TableViewCell of the style Subtitle which contains a title and a description in each row that would be populated using a Model class.

iOS UISearchController Example Project Structure

iOS UISearchController example

Storyboard

UISearchController storyboard

UISearchController example code

Create a new file Model.swift that stores the data for each row.


import Foundation

struct Model {
    let movie : String
    let genre : String
}

The ViewController.swift source code is given below.


import UIKit

class ViewController: UITableViewController {

    var models = [Model]()
    var filteredModels = [Model]()
    let searchController = UISearchController(searchResultsController: nil)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        self.tableView.tableFooterView = UIView()
        
        setupSearchController()
        
        models = [
            Model(movie:"The Dark Night", genre:"Action"),
            Model(movie:"The Avengers", genre:"Action"),
            Model(movie:"Logan", genre:"Action"),
            Model(movie:"Shutter Island", genre:"Thriller"),
            Model(movie:"Inception", genre:"Thriller"),
            Model(movie:"Titanic", genre:"Romance"),
            Model(movie:"La la Land", genre:"Romance"),
            Model(movie:"Gone with the Wind", genre:"Romance"),
            Model(movie:"Godfather", genre:"Drama"),
            Model(movie:"Moonlight", genre:"Drama")
        ]
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        let model: Model
        if searchController.isActive && searchController.searchBar.text != "" {
            model = filteredModels[indexPath.row]
        } else {
            model = models[indexPath.row]
        }
        cell.textLabel!.text = model.movie
        cell.detailTextLabel!.text = model.genre
        return cell
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if searchController.isActive && searchController.searchBar.text != "" {
            return filteredModels.count
        }
        
        return models.count
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func setupSearchController() {
        definesPresentationContext = true
        searchController.dimsBackgroundDuringPresentation = false
        searchController.searchResultsUpdater = self
        searchController.searchBar.barTintColor = UIColor(white: 0.9, alpha: 0.9)
        searchController.searchBar.placeholder = "Search by movie name or genre"
        searchController.hidesNavigationBarDuringPresentation = false
        
        tableView.tableHeaderView = searchController.searchBar
    }
    
    
    
    func filterRowsForSearchedText(_ searchText: String) {
        filteredModels = models.filter({( model : Model) -> Bool in
            return model.movie.lowercased().contains(searchText.lowercased())||model.genre.lowercased().contains(searchText.lowercased())
        })
        tableView.reloadData()
    }


    
    
}
extension ViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        if let term = searchController.searchBar.text {
            filterRowsForSearchedText(term)
        }
    }
}

Pheww! That’s a big code. Let’s understand this step by step.

  1. The ViewController class implements UITableViewController. So there’s no need to implement the Delegate and DataSource protocols separately
  2. The models array would contain the data for all the rows. The filteredModels array would contain the data for the rows that match the searched term based on the condition we’d be setting
  3. The line that initialises the UISearchController is:
    
    let searchController = UISearchController(searchResultsController: nil)
    

    By setting the searchResultsController to nil we state that the search results would be updated in the current TableViewController itself rather than a different View.

  4. To get rid of the empty rows we call:
    
    self.tableView.tableFooterView = UIView()
    
    • Let’s setup a few features on the SearchController.
      
      func setupSearchController() {
              definesPresentationContext = true
              searchController.dimsBackgroundDuringPresentation = false
              searchController.searchResultsUpdater = self
              searchController.searchBar.barTintColor = UIColor(white: 0.9, alpha: 0.9)
              searchController.searchBar.placeholder = "Search by movie name or genre"
              searchController.hidesNavigationBarDuringPresentation = false
              
              tableView.tableHeaderView = searchController.searchBar
          }
      

      By setting definesPresentationContext to true we ensure that the search controller is presented within the bounds of the original table view controller.

    • searchResultsUpdater conforms to the UISearchResultsUpdating protocol. Setting this makes sure that the updateSearchResults function present in the extension would be invoked everytime text in searchBar changes.
  1. The TableView cellForRowAt and numberOfRowsInSection display the relevant data from the filteredModels or models class if the searchBar has some text or not.
  2. The filerRowsForSearchedText function is invoked every time text changes inside the method updateSearchResults.

    
    
    func filterRowsForSearchedText(_ searchText: String) {
            filteredModels = models.filter({( model : Model) -> Bool in
                return model.movie.lowercased().contains(searchText.lowercased())||model.genre.lowercased().contains(searchText.lowercased())
            })
            tableView.reloadData()
        }
    

    In the above function, we filtered the models array and copy only those elements into filteredModels which are a substring of the models.movie or models.genre class.

  3. In the above code, the part inside the filter method has a different syntax. It’s called a closure.
    
    {( model : Model) -> Bool in
                return model.movie.lowercased().contains(searchText.lowercased())||model.genre.lowercased().contains(searchText.lowercased())
            }
    

    filter() takes a closure of type (model: Model) -> Bool and iterates over the entire array, returning only those elements that match the condition.

What are closures?

Closures are sort of anonymous functions without the keyword func. The in keyword is used to separate the input parameters from the return part.

An example of closures is:


var closureToAdd: (Int, Int) -> Int = { (a, b) in
        return a + b
    }
print(closureToAdd(10,5)) //prints 15

The input parameters are Int followed by -> and the return type again an Int.

We can call a closure similar to the way we call functions.

The output of the above application in action is below.

uisearchcontroller example

This brings an end to this tutorial. You can download the iOS UISearchController example project from the link given below.

Reference: Official Documentation

Comments

  1. Caio França says:

    I developed a tableview and a search bar that are ok. The SB works but I’m having a problem when I select the row. Imagine that are three rows: Action, Comedy and Terror. When I type “Com”, it works and the only row now is Comedy, but when I select the row filtered, open a scene with Action informations, So, whatever row I search, always open the scene of the first row, in this case: Action. How can I fix this?

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages