-
-
Save gotelgest/cf309f6e2095ff22a20b09ba5c95be36 to your computer and use it in GitHub Desktop.
import Eureka | |
open class _SearchSelectorViewController<Row: SelectableRowType, OptionsRow: OptionsProviderRow>: SelectorViewController<OptionsRow>, UISearchResultsUpdating where Row.Cell.Value: SearchItem { | |
let searchController = UISearchController(searchResultsController: nil) | |
var originalOptions = [ListCheckRow<Row.Cell.Value>]() | |
var currentOptions = [ListCheckRow<Row.Cell.Value>]() | |
open override func viewDidLoad() { | |
super.viewDidLoad() | |
searchController.searchResultsUpdater = self | |
searchController.dimsBackgroundDuringPresentation = false | |
definesPresentationContext = true | |
if #available(iOS 11.0, *) { | |
navigationItem.searchController = searchController | |
navigationItem.hidesSearchBarWhenScrolling = true | |
} else { | |
tableView.tableHeaderView = searchController.searchBar | |
} | |
} | |
public func updateSearchResults(for searchController: UISearchController) { | |
guard let query = searchController.searchBar.text else { return } | |
if query.isEmpty { | |
currentOptions = originalOptions | |
} else { | |
currentOptions = originalOptions.filter { $0.selectableValue?.matchesSearchQuery(query) ?? false } | |
} | |
tableView.reloadData() | |
} | |
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return currentOptions.count | |
} | |
open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let option = currentOptions[indexPath.row] | |
option.updateCell() | |
return option.baseCell | |
} | |
open override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
return nil | |
} | |
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
currentOptions[indexPath.row].didSelect() | |
tableView.deselectRow(at: indexPath, animated: true) | |
} | |
open override func setupForm(with options: [OptionsRow.OptionsProviderType.Option]) { | |
super.setupForm(with: options) | |
if let allRows = form.first?.map({ $0 }) as? [ListCheckRow<Row.Cell.Value>] { | |
originalOptions = allRows | |
currentOptions = originalOptions | |
} | |
tableView.reloadData() | |
} | |
} | |
open class SearchSelectorViewController<OptionsRow: OptionsProviderRow>: _SearchSelectorViewController<ListCheckRow<OptionsRow.OptionsProviderType.Option>, OptionsRow> where OptionsRow.OptionsProviderType.Option: SearchItem { | |
} | |
open class _SearchPushRow<Cell: CellType> : SelectorRow<Cell> where Cell: BaseCell, Cell.Value : SearchItem { | |
public required init(tag: String?) { | |
super.init(tag: tag) | |
presentationMode = .show(controllerProvider: ControllerProvider.callback { return SearchSelectorViewController<SelectorRow<Cell>> { _ in } }, onDismiss: { vc in | |
let _ = vc.navigationController?.popViewController(animated: true) }) | |
} | |
} | |
public final class SearchPushRow<T: Equatable> : _SearchPushRow<PushSelectorCell<T>>, RowType where T: SearchItem { | |
public required init(tag: String?) { | |
super.init(tag: tag) | |
} | |
} | |
public protocol SearchItem { | |
func matchesSearchQuery(_ query: String) -> Bool | |
} |
Really cool! I just needed to adapt it for use with a form using multiple sections (using sectionKeyForValue). I also needed to keep the titles of sections. So here's my adaptation in case somebody needs it, or if you want to integrate my changes:
import Eureka
open class _SearchSelectorViewController<Row: SelectableRowType, OptionsRow: OptionsProviderRow>: SelectorViewController<OptionsRow>, UISearchResultsUpdating where Row.Cell.Value: SearchItem {
let searchController = UISearchController(searchResultsController: nil)
var originalOptions = [[ListCheckRow<Row.Cell.Value>]]()
var currentOptions = [[ListCheckRow<Row.Cell.Value>]]()
open override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = true
} else {
tableView.tableHeaderView = searchController.searchBar
}
}
public func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text else { return }
if query.isEmpty {
currentOptions = originalOptions
} else {
currentOptions = []
for section in originalOptions {
currentOptions.append(section.filter { $0.selectableValue?.matchesSearchQuery(query) ?? false })
}
}
tableView.reloadData()
}
open override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return currentOptions[section].count == 0 ? 0 : super.tableView(tableView, heightForHeaderInSection: section)
}
open override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return currentOptions[section].count == 0 ? 0 : super.tableView(tableView, heightForFooterInSection: section)
}
open override func numberOfSections(in tableView: UITableView) -> Int {
return currentOptions.count
}
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return currentOptions[section].count
}
open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let option = currentOptions[indexPath.section][indexPath.row]
option.updateCell()
return option.baseCell
}
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
currentOptions[indexPath.section][indexPath.row].didSelect()
tableView.deselectRow(at: indexPath, animated: true)
}
open override func setupForm(with options: [OptionsRow.OptionsProviderType.Option]) {
super.setupForm(with: options)
originalOptions = []
for section in form.allSections {
if let allRows = section.map({ $0 }) as? [ListCheckRow<Row.Cell.Value>] {
originalOptions.append(allRows)
}
}
currentOptions = originalOptions
tableView.reloadData()
}
}
open class SearchSelectorViewController<OptionsRow: OptionsProviderRow>: _SearchSelectorViewController<ListCheckRow<OptionsRow.OptionsProviderType.Option>, OptionsRow> where OptionsRow.OptionsProviderType.Option: SearchItem {
}
open class _SearchPushRow<Cell: CellType> : SelectorRow<Cell> where Cell: BaseCell, Cell.Value : SearchItem {
public required init(tag: String?) {
super.init(tag: tag)
presentationMode = .show(controllerProvider: ControllerProvider.callback { return SearchSelectorViewController<SelectorRow<Cell>> { _ in } }, onDismiss: { vc in
let _ = vc.navigationController?.popViewController(animated: true) })
}
}
public final class SearchPushRow<T: Equatable> : _SearchPushRow<PushSelectorCell<T>>, RowType where T: SearchItem {
public required init(tag: String?) {
super.init(tag: tag)
}
}
public protocol SearchItem {
func matchesSearchQuery(_ query: String) -> Bool
}
Any chance to share multiple selection ?
Thanks for this! It works. I have a little UI issue (if anyone could assist please).
I'd like to get rid of the black bar at the top (which I think is the rest of the space for the navigationItem - since it shrinks when the searchbar textfield is in focus)
Second, I'd like to get rid of what I believe is an empty section header.
I also haven't been able to change the searchBar textfield textColor. This is currently what I have tried:
let textField = searchController.searchBar.value(forKey: "searchField") as? UITextField
textField?.textColor = .white
@benji101 how do you get multiple sections of data in the form so that the loop over form.allSections
in setupForm
actually aggregates the section options?
Here is a version of of this code with the ability to use scope filters in the UISearchController
import Eureka
open class _SearchSelectorViewController<Row: SelectableRowType, OptionsRow: OptionsProviderRow>: SelectorViewController<OptionsRow>, UISearchResultsUpdating, UISearchBarDelegate where Row.Cell.Value: SearchItem {
let searchController = UISearchController(searchResultsController: nil)
var originalOptions = [ListCheckRow<Row.Cell.Value>]()
var currentOptions = [ListCheckRow<Row.Cell.Value>]()
var scopeTitles: [String]?
var showAllScope = true
private let allScopeTitle = "All"
open override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
if let scopes = scopeTitles {
searchController.searchBar.scopeButtonTitles = showAllScope ? [allScopeTitle] + scopes : scopes
searchController.searchBar.delegate = self
}
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = true
} else {
tableView.tableHeaderView = searchController.searchBar
}
}
private func filterOptionsForSearchText(_ searchText: String, scope: String?) {
if searchText.isEmpty {
currentOptions = scope == nil ? originalOptions : originalOptions.filter { item in
guard let value = item.selectableValue else { return false }
return (scope == allScopeTitle) || value.matchesScope(scope!)
}
} else if scope == nil {
currentOptions = originalOptions.filter { $0.selectableValue?.matchesSearchQuery(searchText) ?? false}
} else {
currentOptions = originalOptions.filter { item in
guard let value = item.selectableValue else { return false }
let doesScopeMatch = (scope == allScopeTitle) || value.matchesScope(scope!)
return doesScopeMatch && value.matchesSearchQuery(searchText)
}
}
}
public func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
guard let query = searchBar.text else { return }
let scope = searchBar.scopeButtonTitles?[searchBar.selectedScopeButtonIndex]
filterOptionsForSearchText(query, scope: scope)
tableView.reloadData()
}
public func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterOptionsForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles?[selectedScope])
tableView.reloadData()
}
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return currentOptions.count
}
open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let option = currentOptions[indexPath.row]
option.updateCell()
return option.baseCell
}
open override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return nil
}
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
currentOptions[indexPath.row].didSelect()
tableView.deselectRow(at: indexPath, animated: true)
}
open override func setupForm(with options: [OptionsRow.OptionsProviderType.Option]) {
super.setupForm(with: options)
if let allRows = form.first?.map({ $0 }) as? [ListCheckRow<Row.Cell.Value>] {
originalOptions = allRows
currentOptions = originalOptions
}
tableView.reloadData()
}
}
open class SearchSelectorViewController<OptionsRow: OptionsProviderRow>: _SearchSelectorViewController<ListCheckRow<OptionsRow.OptionsProviderType.Option>, OptionsRow> where OptionsRow.OptionsProviderType.Option: SearchItem {
}
open class _SearchPushRow<Cell: CellType> : SelectorRow<Cell> where Cell: BaseCell, Cell.Value : SearchItem {
/// The scopes to use for additional filtering
open var scopeTitles: [String]?
/// If `true` show the All scope button, else hide it
open var showAllScope = true
public required init(tag: String?) {
super.init(tag: tag)
presentationMode = .show(controllerProvider: ControllerProvider.callback {
let svc = SearchSelectorViewController<SelectorRow<Cell>> { _ in }
svc.scopeTitles = self.scopeTitles
svc.showAllScope = self.showAllScope
return svc }, onDismiss: { vc in let _ = vc.navigationController?.popViewController(animated: true)
})
}
}
public final class SearchPushRow<T: Equatable> : _SearchPushRow<PushSelectorCell<T>>, RowType where T: SearchItem {
public required init(tag: String?) {
super.init(tag: tag)
}
}
public protocol SearchItem {
func matchesSearchQuery(_ query: String) -> Bool
func matchesScope(_ scopeName: String) -> Bool
}
extension SearchItem {
func matchesScope(_ scopeName: String) -> Bool {
return true
}
}
Example code:
enum Category: String, CaseIterable {
case one = "One"
case two = "Two"
}
struct Project: SearchItem, Equatable, CustomStringConvertible {
var description: String {
return "Project: \(id)"
}
let id: String
let category: Category
func matchesSearchQuery(_ query: String) -> Bool {
return id.lowercased().contains(query.lowercased())
}
func matchesScope(_ scopeName: String) -> Bool {
let category = Category(rawValue: scopeName)!
return self.category == category
}
}
class ViewController: FormViewController {
let options = [
Project(id: "A123", category: .one),
Project(id: "B456", category: .two),
Project(id: "A789", category: .one),
Project(id: "B375", category: .two),
Project(id: "C477", category: .one)
]
override func viewDidLoad() {
super.viewDidLoad()
form +++ SearchPushRow<Project>("project") {
$0.title = "Project"
$0.options = options
$0.scopeTitles = [Category.one.rawValue, Category.two.rawValue]
}
}
}
Hey thanks for sharing . It worked like a charm. Can we implement case-Insensitive search on this? Please suggest the steps or example if any !
@PreetikaSingh I'm using the matchesSearchQuery
method on my SearchItem
implementer (the Project
struct) to do the case-insensitive matching. You can see this in the "Example code" section:
func matchesSearchQuery(_ query: String) -> Bool {
return id.lowercased().contains(query.lowercased())
}
The above code is not compiling on Swift 4.2 and Eureka 4.3. Can anybody help me with this.
Did anybody manage to get a version of this to work with both sectionKeyForValue (@benji101 version) and lazy loading? Because when I lazy load in @benji101's version I get a crash:
'NSInternalInconsistencyException', reason: 'attempt to insert section 0 but there are only 0 sections after the update'
And I can't figure out how to fix it.
Awesome, thanks for the great gist. Now how can I implement this to the MultipleSelectorRow ?