Search integrated in table view header
This commit is contained in:
@@ -326,11 +326,6 @@
|
|||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
<barButtonItem enabled="NO" title="7 days" id="wxA-bC-1pN"/>
|
<barButtonItem enabled="NO" title="7 days" id="wxA-bC-1pN"/>
|
||||||
</leftBarButtonItems>
|
</leftBarButtonItems>
|
||||||
<barButtonItem key="rightBarButtonItem" systemItem="search" id="FHY-of-M4V">
|
|
||||||
<connections>
|
|
||||||
<action selector="searchButtonTapped:" destination="pdd-aM-sKl" id="HH1-6f-mcM"/>
|
|
||||||
</connections>
|
|
||||||
</barButtonItem>
|
|
||||||
</navigationItem>
|
</navigationItem>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="filterButton" destination="FZm-Ld-jJE" id="g96-Q2-cYX"/>
|
<outlet property="filterButton" destination="FZm-Ld-jJE" id="g96-Q2-cYX"/>
|
||||||
@@ -383,13 +378,7 @@
|
|||||||
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
|
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableView>
|
</tableView>
|
||||||
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05">
|
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05"/>
|
||||||
<barButtonItem key="rightBarButtonItem" systemItem="search" id="cBL-dP-ig1">
|
|
||||||
<connections>
|
|
||||||
<action selector="searchButtonTapped:" destination="WcC-nb-Vf5" id="QFl-Me-lc6"/>
|
|
||||||
</connections>
|
|
||||||
</barButtonItem>
|
|
||||||
</navigationItem>
|
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
|
|||||||
@@ -1,107 +1,48 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// Assigns a `UISearchBar` to the `tableHeaderView` property of a `UITableView`.
|
class SearchBarManager: NSObject, UISearchResultsUpdating {
|
||||||
class SearchBarManager: NSObject, UISearchBarDelegate {
|
|
||||||
|
|
||||||
private weak var tableView: UITableView?
|
private(set) var isActive = false
|
||||||
private let searchBar: UISearchBar
|
private(set) var term = ""
|
||||||
private(set) var active: Bool = false
|
private lazy var controller: UISearchController = {
|
||||||
|
let x = UISearchController(searchResultsController: nil)
|
||||||
typealias OnChange = (String) -> Void
|
x.searchBar.autocapitalizationType = .none
|
||||||
typealias OnHide = () -> Void
|
x.searchBar.autocorrectionType = .no
|
||||||
private var onChangeCallback: OnChange!
|
x.obscuresBackgroundDuringPresentation = false
|
||||||
private var onHideCallback: OnHide?
|
x.searchResultsUpdater = self
|
||||||
|
return x
|
||||||
|
}()
|
||||||
|
private weak var tvc: UITableViewController?
|
||||||
|
private let onChangeCallback: (String) -> Void
|
||||||
|
|
||||||
/// Prepare `UISearchBar` for user input
|
/// Prepare `UISearchBar` for user input
|
||||||
/// - Parameter tableView: The `tableHeaderView` property is used for display.
|
/// - Parameter onChange: Code that will be executed every time the user changes the text (with 0.2s delay)
|
||||||
required init(on tableView: UITableView) {
|
required init(onChange: @escaping (String) -> Void) {
|
||||||
self.tableView = tableView
|
onChangeCallback = onChange
|
||||||
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
|
|
||||||
super.init()
|
super.init()
|
||||||
searchBar.delegate = self
|
|
||||||
|
|
||||||
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self])
|
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self])
|
||||||
.defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)]
|
.defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assigns the `UISearchBar` to `tableView.tableHeaderView` (iOS 9) or `navigationItem.searchController` (iOS 11).
|
||||||
// MARK: Show & Hide
|
func fuseWith(tableViewController: UITableViewController?) {
|
||||||
|
guard tvc !== tableViewController else { return }
|
||||||
/// Insert search bar in `tableView` and call `reloadData()` after animation.
|
tvc = tableViewController
|
||||||
/// - Parameters:
|
|
||||||
/// - onHide: Code that will be executed once the search bar is dismissed.
|
if #available(iOS 11.0, *) {
|
||||||
/// - onChange: Code that will be executed every time the user changes the text (with 0.2s delay)
|
tvc?.navigationItem.searchController = controller
|
||||||
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()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
searchBar.resignFirstResponder()
|
controller.loadViewIfNeeded() // Fix: "Attempting to load the view of a view controller while it is deallocating"
|
||||||
UIView.animate(withDuration: 0.3, animations: {
|
tvc?.definesPresentationContext = true // make search bar disappear if user changes scene (eg. select cell)
|
||||||
tv.frame.origin.y -= h
|
//tvc?.tableView.backgroundView = UIView() // iOS 11+ bug: bright white background in dark mode
|
||||||
tv.frame.size.height += h
|
tvc?.tableView.tableHeaderView = controller.searchBar
|
||||||
tv.scrollToTop(animated: false) // false to let UIView animate the change
|
tvc?.tableView.setContentOffset(.init(x: 0, y: controller.searchBar.frame.height), animated: false)
|
||||||
}) { _ in
|
|
||||||
tv.frame.origin.y += h
|
|
||||||
tv.frame.size.height -= h
|
|
||||||
self.hideAndRelease()
|
|
||||||
tv.reloadData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call `OnHide` closure (if set), then release strong closure references.
|
/// Search callback
|
||||||
private func hideAndRelease() {
|
func updateSearchResults(for controller: UISearchController) {
|
||||||
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) {
|
|
||||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
||||||
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
|
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
|
||||||
}
|
}
|
||||||
@@ -109,7 +50,8 @@ class SearchBarManager: NSObject, UISearchBarDelegate {
|
|||||||
/// Internal callback function for delayed text evaluation.
|
/// Internal callback function for delayed text evaluation.
|
||||||
/// This way we can avoid unnecessary searches while user is typing.
|
/// This way we can avoid unnecessary searches while user is typing.
|
||||||
@objc private func performSearch() {
|
@objc private func performSearch() {
|
||||||
onChangeCallback(searchBar.text ?? "")
|
term = controller.searchBar.text?.lowercased() ?? ""
|
||||||
tableView?.reloadData()
|
isActive = term.count > 0
|
||||||
|
onChangeCallback(term)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
|
|||||||
|
|
||||||
let parent: String?
|
let parent: String?
|
||||||
private let pipeline = FilterPipeline<GroupedDomain>()
|
private let pipeline = FilterPipeline<GroupedDomain>()
|
||||||
private lazy var search = SearchBarManager(on: delegate!.tableView)
|
|
||||||
private var currentOrder: DateFilterOrderBy = .Date
|
private var currentOrder: DateFilterOrderBy = .Date
|
||||||
private var orderAsc = false
|
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.
|
/// Will init `sync.allowPullToRefresh()` on `tableView.refreshControl` as well.
|
||||||
weak var delegate: GroupedDomainDataSourceDelegate? {
|
weak var delegate: GroupedDomainDataSourceDelegate? {
|
||||||
willSet { if #available(iOS 10.0, *), newValue !== delegate {
|
willSet { if #available(iOS 10.0, *), newValue !== delegate {
|
||||||
@@ -28,6 +31,13 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
|
|||||||
/// - Note: Will call `tableview.reloadData()`
|
/// - Note: Will call `tableview.reloadData()`
|
||||||
init(withParent: String?) {
|
init(withParent: String?) {
|
||||||
parent = withParent
|
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
|
pipeline.delegate = self
|
||||||
resetSortingOrder(force: true)
|
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
|
// # MARK: - Edit Row
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS
|
|||||||
source.delegate = self // init lazy var, ready for tableView data source
|
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?) {
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
if let index = tableView.indexPathForSelectedRow?.row {
|
if let index = tableView.indexPathForSelectedRow?.row {
|
||||||
(segue.destination as? TVCHosts)?.parentDomain = source[index].domain
|
(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
|
// MARK: - Filter
|
||||||
|
|
||||||
@IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) {
|
@IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate {
|
|||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
isSpecial = (parentDomain.first == "#") // aka: "# IP address"
|
isSpecial = (parentDomain.first == "#") // aka: "# IP address"
|
||||||
source.delegate = self // init lazy var, ready for tableView data source
|
source.delegate = self // init lazy var, ready for tableView data source
|
||||||
|
source.search.fuseWith(tableViewController: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
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
|
// MARK: - Table View Data Source
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user