Integrating Search using the iOS 17 UISearchController

The previous chapters have covered creating a table view using prototype cells and introduced table view navigation using a navigation controller. In this final chapter dedicated to table views and table view navigation, we 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.

The app can intercept a wide range of notifications relating to the user’s interaction with the search bar by assigning classes that conform to the UISearchControllerDelegate and UISearchBarDelegate protocols.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Integrating the search controller into a navigation bar 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, as shown in Figure 30-1:

Figure 30-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)
.
.Code language: Swift (swift)

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" 
}Code language: Swift (swift)

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:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func initialize() {
.
.
    navigationItem.searchController = searchController
    definesPresentationContext = true
}Code language: Swift (swift)

The definesPresentationContext setting is a property of the view controller and ensures that any view controllers displayed from the current controller can 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()
}Code language: Swift (swift)

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 is 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 in the matches array. Finally, the searching variable is set to true, and the table view data is reloaded.

Reporting the Number of Table Rows

Since the AttractionTableViewController is being used to display the full list of attractions and the search results, the number of rows displayed will 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:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

override func tableView(_ tableView: UITableView, 
		numberOfRowsInSection section: Int) -> Int {
    return searching ? matches.count : attractionNames.count
}Code language: Swift (swift)

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 ensuring that the tableView(_:cellForRowAt:) method returns the appropriate cells when the view controller displays 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: UIFont.TextStyle.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
}Code language: Swift (swift)

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 users 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)
                    self.updateSearchResults(for: self.searchController)
                } else {
                    self.attractionNames.remove(at: row)
                    self.attractionImages.remove(at: row)
                    self.webAddresses.remove(at: row)
                }
                completionHandler(true)
                tableView.reloadData()
        })
    ])
    return configuration
}Code language: Swift (swift)

Modifying the Detail Segue

When search results are displayed in the table view, the user can still 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 WebKit View. The prepare(forSegue:) method now needs to be 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:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

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]
    }
}Code language: Swift (swift)

Handling the Search Cancel Button

The final task is to ensure the view controller switches out of search mode when the user clicks the Cancel button in the search bar and displays the complete 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()
}Code language: Swift (swift)

Testing the Search Controller

Build and run the app, begin entering text into the search bar, and confirm that the list of results narrows with each keystroke to display only matching attractions:

Figure 30-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. Then, 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 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.


Categories