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