From 171dabd83ae45c3c36817c37c0a5a08b2bd74ccb Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 21 Jun 2020 16:13:58 +0200 Subject: [PATCH] Search integrated in table view header --- main/Base.lproj/Main.storyboard | 13 +- main/Common Classes/SearchBarManager.swift | 124 +++++------------- .../Data Source/GroupedDomainDataSource.swift | 43 ++---- main/Requests/TVCDomains.swift | 12 +- main/Requests/TVCHosts.swift | 6 +- 5 files changed, 51 insertions(+), 147 deletions(-) diff --git a/main/Base.lproj/Main.storyboard b/main/Base.lproj/Main.storyboard index fcee5bc..9254845 100644 --- a/main/Base.lproj/Main.storyboard +++ b/main/Base.lproj/Main.storyboard @@ -326,11 +326,6 @@ - - - - - @@ -383,13 +378,7 @@ - - - - - - - + diff --git a/main/Common Classes/SearchBarManager.swift b/main/Common Classes/SearchBarManager.swift index 5880de5..7d1bc48 100644 --- a/main/Common Classes/SearchBarManager.swift +++ b/main/Common Classes/SearchBarManager.swift @@ -1,107 +1,48 @@ import UIKit -/// Assigns a `UISearchBar` to the `tableHeaderView` property of a `UITableView`. -class SearchBarManager: NSObject, UISearchBarDelegate { +class SearchBarManager: NSObject, UISearchResultsUpdating { - private weak var tableView: UITableView? - private let searchBar: UISearchBar - private(set) var active: Bool = false - - typealias OnChange = (String) -> Void - typealias OnHide = () -> Void - private var onChangeCallback: OnChange! - private var onHideCallback: OnHide? + 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 tableView: The `tableHeaderView` property is used for display. - required init(on tableView: UITableView) { - self.tableView = tableView - searchBar = UISearchBar(frame: CGRect.init(x: 0, y: 0, width: 20, height: 10)) - searchBar.sizeToFit() // sets height, width is set by table view header - searchBar.showsCancelButton = true - searchBar.autocapitalizationType = .none - searchBar.autocorrectionType = .no + /// - 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() - searchBar.delegate = self UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]) .defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)] } - - // MARK: Show & Hide - - /// Insert search bar in `tableView` and call `reloadData()` after animation. - /// - Parameters: - /// - onHide: Code that will be executed once the search bar is dismissed. - /// - onChange: Code that will be executed every time the user changes the text (with 0.2s delay) - func show(onHide: OnHide? = nil, onChange: @escaping OnChange) { - onChangeCallback = onChange - onHideCallback = onHide - setSearchBarHidden(false) - } - - /// Remove search bar from `tableView` and call `reloadData()` after animation. - func hide() { - setSearchBarHidden(true) - } - - /// Internal method to insert or remove the `UISearchBar` as `tableHeaderView` - private func setSearchBarHidden(_ flag: Bool) { - active = !flag - searchBar.text = nil - guard let tv = tableView else { - hideAndRelease() - return - } - let h = searchBar.frame.height - if active { - tv.scrollToTop(animated: false) - tv.tableHeaderView = searchBar - tv.frame.origin.y -= h - tv.frame.size.height += h - UIView.animate(withDuration: 0.3, animations: { - tv.frame.origin.y += h - tv.frame.size.height -= h - }) { _ in - tv.reloadData() - self.searchBar.becomeFirstResponder() - } + /// 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 { - searchBar.resignFirstResponder() - UIView.animate(withDuration: 0.3, animations: { - tv.frame.origin.y -= h - tv.frame.size.height += h - tv.scrollToTop(animated: false) // false to let UIView animate the change - }) { _ in - tv.frame.origin.y += h - tv.frame.size.height -= h - self.hideAndRelease() - tv.reloadData() - } + 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) } } - /// Call `OnHide` closure (if set), then release strong closure references. - private func hideAndRelease() { - tableView?.tableHeaderView = nil - onHideCallback?() - onHideCallback = nil - onChangeCallback = nil - } - - - // MARK: Search Bar Delegate - - func searchBarCancelButtonClicked(_ _: UISearchBar) { - setSearchBarHidden(true) - } - - func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { - searchBar.resignFirstResponder() - } - - func searchBar(_ _: UISearchBar, textDidChange _: String) { + /// Search callback + func updateSearchResults(for controller: UISearchController) { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil) perform(#selector(performSearch), with: nil, afterDelay: 0.2) } @@ -109,7 +50,8 @@ class SearchBarManager: NSObject, UISearchBarDelegate { /// Internal callback function for delayed text evaluation. /// This way we can avoid unnecessary searches while user is typing. @objc private func performSearch() { - onChangeCallback(searchBar.text ?? "") - tableView?.reloadData() + term = controller.searchBar.text?.lowercased() ?? "" + isActive = term.count > 0 + onChangeCallback(term) } } diff --git a/main/Data Source/GroupedDomainDataSource.swift b/main/Data Source/GroupedDomainDataSource.swift index fb60e09..6f49a20 100644 --- a/main/Data Source/GroupedDomainDataSource.swift +++ b/main/Data Source/GroupedDomainDataSource.swift @@ -15,10 +15,13 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate { let parent: String? private let pipeline = FilterPipeline() - private lazy var search = SearchBarManager(on: delegate!.tableView) private var currentOrder: DateFilterOrderBy = .Date private var orderAsc = false + private(set) lazy var search = SearchBarManager { [unowned self] _ in + self.pipeline.reloadFilter(withId: "search") + } + /// Will init `sync.allowPullToRefresh()` on `tableView.refreshControl` as well. weak var delegate: GroupedDomainDataSourceDelegate? { willSet { if #available(iOS 10.0, *), newValue !== delegate { @@ -28,6 +31,13 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate { /// - Note: Will call `tableview.reloadData()` init(withParent: String?) { parent = withParent + let len: Int + if let p = withParent, p.first != "#" { len = p.count } else { len = 0 } + + pipeline.addFilter("search") { [unowned self] in + !self.search.isActive || + $0.domain.prefix($0.domain.count - len).lowercased().contains(self.search.term) + } pipeline.delegate = self resetSortingOrder(force: true) @@ -222,37 +232,6 @@ extension GroupedDomainDataSource { } -// ################################ -// # -// # MARK: - Search -// # -// ################################ - -extension GroupedDomainDataSource { - // TODO: permanently show search bar as table header? - func toggleSearch() { - if search.active { search.hide() } - else { - // Begin animations group. Otherwise the `scrollToTop` animation is broken. - // This is due to `addFilter` calling `reloadData()` before `search.show()` can animate it. - cellAnimationsGroup() - var searchTerm = "" - let len = parent?.count ?? 0 - pipeline.addFilter("search") { - $0.domain.prefix($0.domain.count - len).lowercased().contains(searchTerm) - } - search.show(onHide: { [unowned self] in - self.pipeline.removeFilter(withId: "search") - }, onChange: { [unowned self] in - searchTerm = $0.lowercased() - self.pipeline.reloadFilter(withId: "search") - }) - cellAnimationsCommit() - } - } -} - - // ########################## // # // # MARK: - Edit Row diff --git a/main/Requests/TVCDomains.swift b/main/Requests/TVCDomains.swift index 5a6a5b3..7aec3ef 100644 --- a/main/Requests/TVCDomains.swift +++ b/main/Requests/TVCDomains.swift @@ -14,6 +14,11 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS source.delegate = self // init lazy var, ready for tableView data source } + override func viewDidAppear(_ animated: Bool) { + // iOS 11+ fix: fuse after `didAppear` to hide on app launch + source.search.fuseWith(tableViewController: self) + } + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let index = tableView.indexPathForSelectedRow?.row { (segue.destination as? TVCHosts)?.parentDomain = source[index].domain @@ -21,13 +26,6 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS } - // MARK: - Search - - @IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) { - source.toggleSearch() - } - - // MARK: - Filter @IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) { diff --git a/main/Requests/TVCHosts.swift b/main/Requests/TVCHosts.swift index d23a7ef..6425027 100644 --- a/main/Requests/TVCHosts.swift +++ b/main/Requests/TVCHosts.swift @@ -12,6 +12,7 @@ class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate { super.viewDidLoad() isSpecial = (parentDomain.first == "#") // aka: "# IP address" source.delegate = self // init lazy var, ready for tableView data source + source.search.fuseWith(tableViewController: self) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { @@ -20,11 +21,6 @@ class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate { } } - // MARK: - Search - - @IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) { - source.toggleSearch() - } // MARK: - Table View Data Source