Implementing Search Capabilities with UISearchController
UISearchController
is a UIKit class introduced in iOS 8 that provides a standard interface for implementing search functionality within an app. It integrates a search bar with a results view controller, handling the presentation and updating of search results. This document covers how to implement search capabilities using UISearchController
in UIKit-based iOS applications.
Purpose
- Search Interface: Provides a native search bar and results UI for filtering content.
- Integration: Seamlessly integrates with
UIViewController
,UITableView
, orUICollectionView
. - User Experience: Supports animated transitions, scope buttons, and search suggestions.
Key Features
- Search Bar: A customizable
UISearchBar
for user input. - Results Controller: A separate view controller to display search results.
- Delegate and Data Source: Uses delegates to handle search interactions and update results.
- Scope Buttons: Supports segmented controls for filtering search results.
- Automatic Management: Handles keyboard, focus, and UI transitions.
Basic Implementation
To implement search, configure a UISearchController
, set its properties, and integrate it into a view controller, typically with a table view to display results.
Example: Searching a Table View
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating {
@IBOutlet weak var tableView: UITableView!
let data = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
var filteredData: [String] = []
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
// Setup table view
tableView.dataSource = self
tableView.delegate = self
// Setup search controller
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Fruits"
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true
filteredData = data
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = filteredData[indexPath.row]
return cell
}
// MARK: - UISearchResultsUpdating
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text, !searchText.isEmpty else {
filteredData = data
tableView.reloadData()
return
}
filteredData = data.filter { $0.lowercased().contains(searchText.lowercased()) }
tableView.reloadData()
}
}
Key Points
- Initialization: Create a
UISearchController
with asearchResultsController
(optional;nil
uses the same view controller for results). - Search Results Updater: Set
searchResultsUpdater
to a class conforming toUISearchResultsUpdating
to handle search input. - Integration: Assign the search controller to
navigationItem.searchController
for navigation bar integration. - Background Obscuring: Set
obscuresBackgroundDuringPresentation
tofalse
to keep the underlying content visible during search. - Presentation Context: Set
definesPresentationContext = true
to ensure the search bar remains in the correct context.
Advanced Features
Using a Separate Results Controller
Display search results in a dedicated view controller:
class SearchResultsController: UITableViewController {
var filteredData: [String] = []
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = filteredData[indexPath.row]
return cell
}
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating {
// ... other properties as above
override func viewDidLoad() {
super.viewDidLoad()
let resultsController = SearchResultsController()
let searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = true
navigationItem.searchController = searchController
definesPresentationContext = true
}
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text,
let resultsController = searchController.searchResultsController as? SearchResultsController else {
return
}
resultsController.filteredData = data.filter { $0.lowercased().contains(searchText.lowercased()) }
resultsController.tableView.reloadData()
}
}
Adding Scope Buttons
Use scope buttons to filter search results by category:
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchBar.scopeButtonTitles = ["All", "Short", "Long"]
searchController.searchBar.delegate = self
// ... other setup
}
// MARK: - UISearchBarDelegate
extension ViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContent(for: searchBar.text ?? "", scope: selectedScope)
}
func updateSearchResults(for searchController: UISearchController) {
let scope = searchController.searchBar.selectedScopeButtonIndex
filterContent(for: searchController.searchBar.text ?? "", scope: scope)
}
func filterContent(for searchText: String, scope: Int) {
filteredData = data.filter { item in
let matchesSearch = searchText.isEmpty || item.lowercased().contains(searchText.lowercased())
switch scope {
case 1: // Short
return matchesSearch && item.count <= 5
case 2: // Long
return matchesSearch && item.count > 5
default: // All
return matchesSearch
}
}
tableView.reloadData()
}
}
Customizing the Search Bar
Customize the search bar’s appearance and behavior:
searchController.searchBar.tintColor = .systemBlue
searchController.searchBar.searchTextField.backgroundColor = .white
searchController.searchBar.showsCancelButton = true
searchController.searchBar.autocapitalizationType = .none
Handling Search Bar Events
Use UISearchBarDelegate
to respond to events like cancel or search button clicks:
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
filteredData = data
tableView.reloadData()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
Hiding the Search Bar
Control the search bar’s visibility:
navigationItem.hidesSearchBarWhenScrolling = true // Hide when scrolling
Best Practices
- Performance: Optimize filtering logic for large datasets to avoid UI lag.
- Delegate Management: Ensure the
searchResultsUpdater
andsearchBar.delegate
are set correctly. - Accessibility: Ensure the search bar and results are VoiceOver-compatible.
- Clear Feedback: Update results in real-time as the user types for a responsive experience.
- Scope Clarity: Use clear, concise scope button titles if filtering by category.
- Testing: Test on iPhone and iPad, in various orientations and with different data sizes.
- Minimal Customization: Avoid excessive search bar styling to maintain system consistency.
Limitations
- iOS 8+:
UISearchController
is available only on iOS 8 and later. - Single Search Bar: Only one search bar is supported per
UISearchController
. - Results Controller: Using a separate results controller increases complexity for state management.
- Large Datasets: Real-time filtering may require optimization for performance.
Summary
UISearchController
provides a powerful, native solution for implementing search in UIKit apps. By configuring the search bar, handling updates via UISearchResultsUpdating
, and optionally using scope buttons or a results controller, developers can create intuitive search experiences. Following best practices ensures performance, accessibility, and a seamless user experience.