import UIKit class SearchBarManager: NSObject, UISearchResultsUpdating { private(set) var isActive = false private(set) var term = "" private lazy var controller: UISearchController = { let x = UISearchController(searchResultsController: nil) x.searchBar.autocapitalizationType = .none x.searchBar.autocorrectionType = .no x.obscuresBackgroundDuringPresentation = false x.searchResultsUpdater = self return x }() private weak var tvc: UITableViewController? private let onChangeCallback: (String) -> Void /// Prepare `UISearchBar` for user input /// - Parameter onChange: Code that will be executed every time the user changes the text (with 0.2s delay) required init(onChange: @escaping (String) -> Void) { onChangeCallback = onChange super.init() UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]) .defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)] } /// Assigns the `UISearchBar` to `tableView.tableHeaderView` (iOS 9) or `navigationItem.searchController` (iOS 11). func fuseWith(tableViewController: UITableViewController?) { guard tvc !== tableViewController else { return } tvc = tableViewController if #available(iOS 11.0, *) { tvc?.navigationItem.searchController = controller } else { let thv = tvc?.tableView.tableHeaderView guard thv == nil || thv is UISearchBar else { // Don't overwrite actions bar (co-occurrence, etc.) // FIXME: find alternative or iOS 9-10 users can't search in hosts tvc = nil return } controller.loadViewIfNeeded() // Fix: "Attempting to load the view of a view controller while it is deallocating" tvc?.definesPresentationContext = true // make search bar disappear if user changes scene (eg. select cell) //tvc?.tableView.backgroundView = UIView() // iOS 11+ bug: bright white background in dark mode tvc?.tableView.tableHeaderView = controller.searchBar tvc?.tableView.setContentOffset(.init(x: 0, y: controller.searchBar.frame.height), animated: false) } } /// Search callback internal func updateSearchResults(for controller: UISearchController) { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil) perform(#selector(performSearch), with: nil, afterDelay: 0.2) } /// Internal callback function for delayed text evaluation. /// This way we can avoid unnecessary searches while user is typing. @objc private func performSearch() { term = controller.searchBar.text?.lowercased() ?? "" isActive = term.count > 0 onChangeCallback(term) } }