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
Storyboard
UISearchController example code
Create a new file Model.swift
that stores the data for each row.
1 2 3 4 5 6 7 |
import Foundation struct Model { let movie : String let genre : String } |
The ViewController.swift
source code is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
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.
- The ViewController class implements UITableViewController. So there’s no need to implement the Delegate and DataSource protocols separately
- 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
- The line that initialises the UISearchController is:
123let searchController = UISearchController(searchResultsController: nil)
By setting thesearchResultsController
to nil we state that the search results would be updated in the current TableViewController itself rather than a different View. - To get rid of the empty rows we call:
123self.tableView.tableFooterView = UIView()
-
- Let’s setup a few features on the SearchController.
1234567891011func setupSearchController() {definesPresentationContext = truesearchController.dimsBackgroundDuringPresentation = falsesearchController.searchResultsUpdater = selfsearchController.searchBar.barTintColor = UIColor(white: 0.9, alpha: 0.9)searchController.searchBar.placeholder = "Search by movie name or genre"searchController.hidesNavigationBarDuringPresentation = falsetableView.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 theUISearchResultsUpdating
protocol. Setting this makes sure that the updateSearchResults function present in the extension would be invoked everytime text in searchBar changes.
- Let’s setup a few features on the SearchController.
- The TableView
cellForRowAt
andnumberOfRowsInSection
display the relevant data from the filteredModels or models class if the searchBar has some text or not. - The
filerRowsForSearchedText
function is invoked every time text changes inside the methodupdateSearchResults
.
12345678func filterRowsForSearchedText(_ searchText: String) {filteredModels = models.filter({( model : Model) -> Bool inreturn model.movie.lowercased().contains(searchText.lowercased())||model.genre.lowercased().contains(searchText.lowercased())})tableView.reloadData()}
In the above function, we filtered themodels
array and copy only those elements intofilteredModels
which are a substring of themodels.movie
ormodels.genre
class. - In the above code, the part inside the filter method has a different syntax. It’s called a
closure
.
12345{( model : Model) -> Bool inreturn 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:
1 2 3 4 5 6 |
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.
This brings an end to this tutorial. You can download the iOS UISearchController example project from the link given below.
Reference: Official Documentation