116 lines
3.5 KiB
Swift
116 lines
3.5 KiB
Swift
import UIKit
|
|
|
|
/// Assigns a `UISearchBar` to the `tableHeaderView` property of a `UITableView`.
|
|
class SearchBarManager: NSObject, UISearchBarDelegate {
|
|
|
|
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?
|
|
|
|
/// 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
|
|
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()
|
|
}
|
|
} 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
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() {
|
|
onChangeCallback(searchBar.text ?? "")
|
|
tableView?.reloadData()
|
|
}
|
|
}
|