Skip to content

Instantly share code, notes, and snippets.

@gotelgest
Last active February 24, 2024 14:35
Show Gist options
  • Save gotelgest/cf309f6e2095ff22a20b09ba5c95be36 to your computer and use it in GitHub Desktop.
Save gotelgest/cf309f6e2095ff22a20b09ba5c95be36 to your computer and use it in GitHub Desktop.
SearchPushRow for Eureka 4.0.1 (Swift 4)
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
}
@hebbian
Copy link

hebbian commented Apr 3, 2018

Awesome, thanks for the great gist. Now how can I implement this to the MultipleSelectorRow ?

@benji101
Copy link

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
}

@radvansky-tomas
Copy link

Any chance to share multiple selection ?

@bernardonigbinde
Copy link

bernardonigbinde commented Jan 9, 2019

Thanks for this! It works. I have a little UI issue (if anyone could assist please).
img_837dade47a92-1

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

@croese
Copy link

croese commented Jan 11, 2019

@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?

@croese
Copy link

croese commented Jan 11, 2019

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]
        }
    }
}

screen shot 2019-01-11 at 9 36 50 am

@PreetikaSingh
Copy link

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 !

@croese
Copy link

croese commented Jan 14, 2019

@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())
}

@rohitucskm
Copy link

rohitucskm commented Jan 17, 2019

The above code is not compiling on Swift 4.2 and Eureka 4.3. Can anybody help me with this.

@sarbogast
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment