Integrating Search using the iOS UISearchController
Learn SwiftUI and take your iOS Development to the Next Level |
The previous chapters have covered the creation of a table view using prototype cells and the introduction of table view navigation using a navigation controller. This, the final chapter dedicated to table views and table view navigation, will cover the integration of a search bar into the navigation bar of the TableViewStory app created in the earlier chapters.
Introducing the UISearchController Class
The UISearchController class is designed to be used alongside existing view controllers to provide a way to integrate search features into apps. The UISearchController class includes a search bar (UISearchBar) into which the user enters the search text.
The search controller is assigned a results updater delegate which must conform to the UISearchResultsUpdating protocol. The updateSearchResults(for searchController:) method of this delegate is called repeatedly as the user enters text into the search bar and is responsible for filtering the results. The results updater object is assigned to the search controller via the controller’s searchResultsUpdater property.
In addition to the results updater, the search controller also needs a view controller to display the search results. The results updater object can also serve as the results view controller, or a separate view controller can be designated for the task via the search controller’s searchViewController property.
A wide range of notifications relating to the user’s interaction with the search bar can be intercepted by the app by assigning classes that conform to the UISearchControllerDelegate and UISearchBarDelegate protocols. In terms of integrating the search controller into a navigation bar, this is achieved by assigning the search controller instance to the searchController property of the navigation bar’s navigation item.
Once all of the configuration criteria have been met, the search bar will appear within the navigation bar when the user scrolls down within the view controller as shown in Figure 31-1:
Figure 31-1
Adding a Search Controller to the TableViewStory Project
To add search to the TableViewStory app, begin by editing the AttractionTableViewController.swift file and adding search related delegate declarations, a UISearchController instance and a new array into which any matching search results will be stored. Also add a Boolean variable that will be used to track whether or not the user is currently performing a search:
class AttractionTableViewController: UITableViewController, UISearchResultsUpdating, UISearchBarDelegate { var attractionImages = [String]() var attractionNames = [String]() var webAddresses = [String]() var searching = false var matches = [Int]() let searchController = UISearchController(searchResultsController: nil) . .
Next, modify the initialize method to designate the table view controller instance as both the search bar and results updater delegates for the search controller. The code also sets properties to display some placeholder text in the search text field and to prevent the search from obscuring the search results view controller:
func initialize() { . . navigationController?.navigationBar.prefersLargeTitles = true searchController.searchBar.delegate = self searchController.searchResultsUpdater = self searchController.obscuresBackgroundDuringPresentation = false searchController.searchBar.placeholder = "Search Attractions" }
With the search controller configured, it can now be added to the navigation item, the code for which can be added at the end of the initialize method as follows:
func initialize() { . . navigationItem.searchController = searchController definesPresentationContext = true }
The definesPresentationContext setting is a property of the view controller and ensures that any view controllers displayed from the current controller will be able to navigate back to the current view controller.
Implementing the updateSearchResults Method
With AttractionTableViewController designated as the results updater delegate, the next step is to implement the updateSearchResults(for searchController:) method within this class file as follows:
func updateSearchResults(for searchController: UISearchController) { if let searchText = searchController.searchBar.text, !searchText.isEmpty { matches.removeAll() for index in 0..<attractionNames.count { if attractionNames[index].lowercased().contains( searchText.lowercased()) { matches.append(index) } } searching = true } else { searching = false } tableView.reloadData() }
The method is passed a reference to the search controller object which contains the text entered into the search bar. The code accesses this text property and verifies that it contains text. If no text has been entered, the method sets the searching variable to false before returning. This variable will be used later to identify if the table view is currently displaying search results or the full list of attractions.
If search text has been entered, any existing entries in the matches array are removed and a for loop used to iterate through each entry within the attractionNames array, checking to see if each name contains the search text. If a match is found, the index value of the matching array item is stored into the matches array.
Finally, the searching variable is set to true and the table view data reloaded.
Reporting the Number of Table Rows
Since the AttractionTableViewController is being used to display both the full list of attractions and the search results, the number of rows displayed will clearly depend on whether or not the controller is in search mode. In search mode, for example, the number of rows will be dictated by the number of items in the matches array. Locate the tableView(_:numberOfRowsInSection:) table view data source delegate method and modify it as follows:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return searching ? matches.count : attractionNames.count }
The method now uses a ternary statement to return either the total number of attractions or the number of matches based on the current value of the searching variable.
Modifying the cellForRowAt Method
The next step is to ensure that the tableView(_:cellForRowAt:) method returns the appropriate cells when the view controller is displaying search results. Specifically, if the user is currently performing a search, the index into the attraction arrays must be taken from the array of index values in the matches array. Modify the method so that it reads as follows:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = self.tableView.dequeueReusableCell(withIdentifier: "AttractionTableCell", for: indexPath) as! AttractionTableViewCell let row = indexPath.row cell.attractionLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline) cell.attractionLabel.text = searching ? attractionNames[matches[row]] : attractionNames[row] let imageName = searching ? attractionImages[matches[row]] : attractionImages[row] cell.attractionImage.image = UIImage(named: imageName) return cell }
Once again ternary statements are being used to control which row index is used based on the prevailing setting of the searching variable.
Modifying the Trailing Swipe Delegate Method
The previous chapter added a trailing swipe delegate method to the table view class to allow the user to delete rows from the table. This method also needs to be updated to allow items to be removed during a search operation. Locate this method in the AttractionTableViewController.swift file and modify it as follows:
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let configuration = UISwipeActionsConfiguration(actions: [ UIContextualAction(style: .destructive, title: "Delete", handler: { (action, view, completionHandler) in let row = indexPath.row if self.searching { self.attractionNames.remove(at: self.matches[row]) self.attractionImages.remove(at: self.matches[row]) self.webAddresses.remove(at: self.matches[row]) self.matches.remove(at: indexPath.row) } else { self.attractionNames.remove(at: row) self.attractionImages.remove(at: row) self.webAddresses.remove(at: row) } completionHandler(true) }) ]) return configuration }
Modifying the Detail Segue
When search results are displayed in the table view, the user will still be able to select an attraction and segue to the details view. When the segue is performed, the URL of the selected attraction is assigned to the webSite property of the DetailViewController class so that the correct page is loaded into the web view. The prepare(forSegue:) method now needs to modified to handle the possibility that the user triggered the segue from a list of search results. Locate this method in the AttractionTableViewController class and modify it as follows:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "ShowAttractionDetails" { let detailViewController = segue.destination as! AttractionDetailViewController let myIndexPath = self.tableView.indexPathForSelectedRow! let row = myIndexPath.row detailViewController.webSite = searching ? webAddresses[matches[row]] : webAddresses[row] } }
Handling the Search Cancel Button
The final task is to make sure when the user clicks the Cancel button in the search bar that the view controller switches out of search mode and displays the full list of attractions. Since the AttractionTableViewController class has already been declared as implementing the UISearchBarDelegate protocol, all that remains is to add the searchBarCancelButtonClicked delegate method in the AttractionTableViewController.swift file. All this method needs to do is set the searching variable to false and instruct the table view to reload data:
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { searching = false tableView.reloadData() }
Testing the Search Controller
Build and run the app and drag the table view down to display the search bar. Begin entering text into the search bar and confirm that the list of results narrows with each key stroke to display only matching attractions:
Figure 31-2
Verify that the correct images are displayed for the attraction results and that selecting an attraction from the list presents the correct web page in the detail view controller. Return to the search results screen and tap the Cancel button to return to the full list of attractions. Also confirm that deleting a row from the search results also removes the item from the full attraction list.
Summary
The UISearchController class provides an easy way to integrate a search bar into iOS apps. When assigned to a navigation item, the search bar appears within the navigation bar of view controllers with an embedded navigation controller. At a minimum, a search controller needs delegates to filter the search and display those results. This chapter has worked through an example search controller implementation in the context of a table view and navigation controller configuration.