Search Hosts + search animations + reload table after filter manipulations
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
54448A2E2486464F00771C96 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2D2486464F00771C96 /* Array.swift */; };
|
54448A2E2486464F00771C96 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2D2486464F00771C96 /* Array.swift */; };
|
||||||
54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; };
|
54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; };
|
||||||
|
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A3124899A4000771C96 /* SearchBarManager.swift */; };
|
||||||
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; };
|
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; };
|
||||||
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
|
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
|
||||||
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
|
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
|
||||||
@@ -180,6 +181,7 @@
|
|||||||
543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = "<group>"; };
|
543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = "<group>"; };
|
||||||
54448A2D2486464F00771C96 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
54448A2D2486464F00771C96 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||||
54448A2F248647D900771C96 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
|
54448A2F248647D900771C96 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
|
||||||
|
54448A3124899A4000771C96 /* SearchBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarManager.swift; sourceTree = "<group>"; };
|
||||||
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
|
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
|
||||||
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.swift; sourceTree = "<group>"; };
|
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.swift; sourceTree = "<group>"; };
|
||||||
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
|
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
|
||||||
@@ -415,6 +417,7 @@
|
|||||||
545DDDD024436983003B6544 /* QuickUI.swift */,
|
545DDDD024436983003B6544 /* QuickUI.swift */,
|
||||||
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
|
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
|
||||||
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
|
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
|
||||||
|
54448A3124899A4000771C96 /* SearchBarManager.swift */,
|
||||||
);
|
);
|
||||||
path = "Common Classes";
|
path = "Common Classes";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -836,6 +839,7 @@
|
|||||||
54D8B97C2471A7E000EB2414 /* String.swift in Sources */,
|
54D8B97C2471A7E000EB2414 /* String.swift in Sources */,
|
||||||
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
|
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
|
||||||
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
|
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
|
||||||
|
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */,
|
||||||
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
|
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
|
||||||
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
|
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
|
||||||
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
|
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
|
||||||
|
|||||||
@@ -342,7 +342,13 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class FilterPipeline<T> {
|
|||||||
pipeline.append(newFilter)
|
pipeline.append(newFilter)
|
||||||
display?.apply(moreRestrictive: newFilter)
|
display?.apply(moreRestrictive: newFilter)
|
||||||
}
|
}
|
||||||
|
if cellAnimations { delegate?.tableView.reloadData() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find and remove filter with given identifier. Will automatically update remaining filters and display sorting.
|
/// Find and remove filter with given identifier. Will automatically update remaining filters and display sorting.
|
||||||
@@ -113,6 +114,7 @@ class FilterPipeline<T> {
|
|||||||
resetFilters(startingAt: i)
|
resetFilters(startingAt: i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cellAnimations { delegate?.tableView.reloadData() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start filter evaluation on all entries from previous filter.
|
/// Start filter evaluation on all entries from previous filter.
|
||||||
@@ -120,19 +122,22 @@ class FilterPipeline<T> {
|
|||||||
if let i = indexOfFilter(ident) {
|
if let i = indexOfFilter(ident) {
|
||||||
resetFilters(startingAt: i)
|
resetFilters(startingAt: i)
|
||||||
}
|
}
|
||||||
|
if cellAnimations { delegate?.tableView.reloadData() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove last `k` filters from the filter pipeline. Thus showing more entries from previous layers.
|
/// Remove last `k` filters from the filter pipeline. Thus showing more entries from previous layers.
|
||||||
func popLastFilter(k: Int = 1) {
|
// func popLastFilter(k: Int = 1) {
|
||||||
guard k > 0, k <= pipeline.count else { return }
|
// guard k > 0, k <= pipeline.count else { return }
|
||||||
pipeline.removeLast(k)
|
// pipeline.removeLast(k)
|
||||||
display?.reset(toLessRestrictive: pipeline.last)
|
// display?.reset(toLessRestrictive: pipeline.last)
|
||||||
}
|
// if cellAnimations { delegate?.tableView.reloadData() }
|
||||||
|
// }
|
||||||
|
|
||||||
/// Sets the sort and display order. You should set the `delegate` to automatically update your `tableView`.
|
/// Sets the sort and display order. You should set the `delegate` to automatically update your `tableView`.
|
||||||
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
||||||
func setSorting(_ predicate: @escaping PipelineSorting<T>.Predicate) {
|
func setSorting(_ predicate: @escaping PipelineSorting<T>.Predicate) {
|
||||||
display = .init(predicate, pipe: self)
|
display = .init(predicate, pipe: self)
|
||||||
|
if cellAnimations { delegate?.tableView.reloadData() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-built filter and display sorting order.
|
/// Re-built filter and display sorting order.
|
||||||
@@ -171,13 +176,13 @@ class FilterPipeline<T> {
|
|||||||
// MARK: data updates
|
// MARK: data updates
|
||||||
|
|
||||||
/// Disable individual cell updates (update, move, insert & remove actions)
|
/// Disable individual cell updates (update, move, insert & remove actions)
|
||||||
func pauseCellAnimations(if condition: Bool) {
|
func pauseCellAnimations(if condition: Bool = true) {
|
||||||
cellAnimations = delegate?.tableView.isFrontmost ?? false && !condition
|
cellAnimations = !condition && delegate?.tableView.isFrontmost ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allow individual cell updates (update, move, insert & remove actions) if tableView `isFrontmost`
|
/// Allow individual cell updates (update, move, insert & remove actions) if tableView `isFrontmost`
|
||||||
/// - Parameter reloadTable: If `true` and cell animations are disabled, perform `tableView.reloadData()`
|
/// - Parameter reloadTable: If `true` and cell animations are disabled, perform `tableView.reloadData()`
|
||||||
func continueCellAnimations(reloadTable: Bool = false) {
|
func continueCellAnimations(reloadTable: Bool = true) {
|
||||||
if !cellAnimations {
|
if !cellAnimations {
|
||||||
cellAnimations = true
|
cellAnimations = true
|
||||||
if reloadTable { delegate?.tableView.reloadData() }
|
if reloadTable { delegate?.tableView.reloadData() }
|
||||||
|
|||||||
110
main/Common Classes/SearchBarManager.swift
Normal file
110
main/Common Classes/SearchBarManager.swift
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
tv.scrollToTop(animated: false)
|
||||||
|
tv.tableHeaderView = searchBar
|
||||||
|
tv.frame.origin.y = -searchBar.frame.height
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
tv.frame.origin.y = 0
|
||||||
|
}) { _ in
|
||||||
|
tv.reloadData()
|
||||||
|
self.searchBar.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchBar.resignFirstResponder()
|
||||||
|
UIView.animate(withDuration: 0.3, animations: {
|
||||||
|
tv.frame.origin.y = -(tv.tableHeaderView?.frame.height ?? 0)
|
||||||
|
tv.scrollToTop(animated: false) // false to let UIView animate the change
|
||||||
|
}) { _ in
|
||||||
|
tv.frame.origin.y = 0
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ class GroupedDomainDataSource {
|
|||||||
|
|
||||||
private let parent: String?
|
private let parent: String?
|
||||||
let pipeline: FilterPipeline<GroupedDomain>
|
let pipeline: FilterPipeline<GroupedDomain>
|
||||||
|
private lazy var search = SearchBarManager(on: pipeline.delegate!.tableView)
|
||||||
|
|
||||||
init(withDelegate tvc: FilterPipelineDelegate, parent p: String?) {
|
init(withDelegate tvc: FilterPipelineDelegate, parent p: String?) {
|
||||||
parent = p
|
parent = p
|
||||||
@@ -179,6 +180,35 @@ extension GroupedDomainDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ################################
|
||||||
|
// #
|
||||||
|
// # MARK: - Search
|
||||||
|
// #
|
||||||
|
// ################################
|
||||||
|
|
||||||
|
extension GroupedDomainDataSource {
|
||||||
|
func toggleSearch() {
|
||||||
|
if search.active { search.hide() }
|
||||||
|
else {
|
||||||
|
// Pause animations. Otherwise the `scrollToTop` animation is broken.
|
||||||
|
// This is due to `addFilter` calling `reloadData()` before `search.show()` can animate it.
|
||||||
|
pipeline.pauseCellAnimations()
|
||||||
|
var searchTerm = ""
|
||||||
|
pipeline.addFilter("search") {
|
||||||
|
$0.domain.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")
|
||||||
|
})
|
||||||
|
pipeline.continueCellAnimations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ##########################
|
// ##########################
|
||||||
// #
|
// #
|
||||||
// # MARK: - Edit Row
|
// # MARK: - Edit Row
|
||||||
|
|||||||
@@ -35,6 +35,19 @@ extension UITableView {
|
|||||||
func safeMoveRow(_ from: Int, to: Int) {
|
func safeMoveRow(_ from: Int, to: Int) {
|
||||||
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
|
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scroll table to top (while respecting `contentInset`)
|
||||||
|
func scrollToTop(animated: Bool) {
|
||||||
|
let top: CGFloat
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
top = adjustedContentInset.top
|
||||||
|
} else {
|
||||||
|
top = contentInset.top
|
||||||
|
}
|
||||||
|
if contentOffset.y != -top {
|
||||||
|
setContentOffset(.init(x: 0, y: -top), animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,11 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, FilterPipelineDele
|
|||||||
|
|
||||||
lazy var source = GroupedDomainDataSource(withDelegate: self, parent: nil)
|
lazy var source = GroupedDomainDataSource(withDelegate: self, parent: nil)
|
||||||
|
|
||||||
private var searchActive: Bool = false
|
|
||||||
private var searchTerm: String?
|
|
||||||
private let searchBar: UISearchBar = {
|
|
||||||
let x = UISearchBar(frame: CGRect.init(x: 0, y: 0, width: 20, height: 10))
|
|
||||||
x.sizeToFit()
|
|
||||||
x.showsCancelButton = true
|
|
||||||
x.autocapitalizationType = .none
|
|
||||||
x.autocorrectionType = .no
|
|
||||||
return x
|
|
||||||
}()
|
|
||||||
@IBOutlet private var filterButton: UIBarButtonItem!
|
@IBOutlet private var filterButton: UIBarButtonItem!
|
||||||
@IBOutlet private var filterButtonDetail: UIBarButtonItem!
|
@IBOutlet private var filterButtonDetail: UIBarButtonItem!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
searchBar.delegate = self
|
|
||||||
NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self)
|
NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self)
|
||||||
didChangeDateFilter()
|
didChangeDateFilter()
|
||||||
}
|
}
|
||||||
@@ -39,62 +28,10 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, FilterPipelineDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Table View Data Source
|
|
||||||
|
|
||||||
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { source.numberOfRows }
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "DomainCell")!
|
|
||||||
let entry = source[indexPath.row]
|
|
||||||
cell.textLabel?.text = entry.domain
|
|
||||||
cell.detailTextLabel?.text = entry.detailCellText
|
|
||||||
cell.imageView?.image = entry.options?.tableRowImage()
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
func rowNeedsUpdate(_ row: Int) {
|
|
||||||
let entry = source[row]
|
|
||||||
let cell = tableView.cellForRow(at: IndexPath(row: row))
|
|
||||||
cell?.detailTextLabel?.text = entry.detailCellText
|
|
||||||
cell?.imageView?.image = entry.options?.tableRowImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Search
|
// MARK: - Search
|
||||||
|
|
||||||
@IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) {
|
@IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) {
|
||||||
setSearch(hidden: searchActive)
|
source.toggleSearch()
|
||||||
}
|
|
||||||
|
|
||||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
|
||||||
setSearch(hidden: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setSearch(hidden: Bool) {
|
|
||||||
searchActive = !hidden
|
|
||||||
searchTerm = nil
|
|
||||||
searchBar.text = nil
|
|
||||||
tableView.tableHeaderView = hidden ? nil : searchBar
|
|
||||||
if searchActive {
|
|
||||||
source.pipeline.addFilter("search") {
|
|
||||||
$0.domain.lowercased().contains(self.searchTerm ?? "")
|
|
||||||
}
|
|
||||||
searchBar.becomeFirstResponder()
|
|
||||||
} else {
|
|
||||||
source.pipeline.removeFilter(withId: "search")
|
|
||||||
}
|
|
||||||
tableView.reloadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
|
||||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
|
||||||
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func performSearch() {
|
|
||||||
searchTerm = searchBar.text?.lowercased() ?? ""
|
|
||||||
source.pipeline.reloadFilter(withId: "search")
|
|
||||||
tableView.reloadData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -124,4 +61,25 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, FilterPipelineDele
|
|||||||
self.filterButton.image = UIImage(named: "filter-clear")
|
self.filterButton.image = UIImage(named: "filter-clear")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Table View Data Source
|
||||||
|
|
||||||
|
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { source.numberOfRows }
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "DomainCell")!
|
||||||
|
let entry = source[indexPath.row]
|
||||||
|
cell.textLabel?.text = entry.domain
|
||||||
|
cell.detailTextLabel?.text = entry.detailCellText
|
||||||
|
cell.imageView?.image = entry.options?.tableRowImage()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func rowNeedsUpdate(_ row: Int) {
|
||||||
|
let entry = source[row]
|
||||||
|
let cell = tableView.cellForRow(at: IndexPath(row: row))
|
||||||
|
cell?.detailTextLabel?.text = entry.detailCellText
|
||||||
|
cell?.imageView?.image = entry.options?.tableRowImage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ class TVCHosts: UITableViewController, FilterPipelineDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Search
|
||||||
|
|
||||||
|
@IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) {
|
||||||
|
source.toggleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Table View Data Source
|
// MARK: - Table View Data Source
|
||||||
|
|
||||||
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { source.numberOfRows }
|
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { source.numberOfRows }
|
||||||
|
|||||||
Reference in New Issue
Block a user