Eureka PushRow With Search Bar

Jul 4, 2016

swift, native, iOS

For my new project I’m using a lot of Data Input and even when Eureka is awesome by itself it’s lacking of search capability in the view controller used by PushRow, the Readme doesn’t provide too much info but after digging up a bit I came with my custom Row that is able to work with a generic type capable of matching with search.

//
//  SearchablePushRow.swift
//

import Eureka

public class _SearchablePushRow<T: Equatable, Cell: CellType where Cell: BaseCell, Cell: TypedCellType, Cell.Value == T, T: SearchableItem, T: CustomStringConvertible> : SelectorRow<T, Cell, SearchableViewController<T>> {

    public required init(tag: String?) {
        super.init(tag: tag)
        presentationMode = .Show(controllerProvider: ControllerProvider.Callback { return SearchableViewController<T>(){ _ in } }, completionCallback: { vc in vc.navigationController?.popViewControllerAnimated(true) })
    }
}

/// Selector Controller (used to select one option among a list)
public class SearchableViewController<T:Equatable where T:SearchableItem, T: CustomStringConvertible> : _SearchableViewController<T, ListCheckRow<T>>  {

    required public init() {
        super.init()
    }
    convenience public init(_ callback: (UIViewController) -> ()){
        self.init()
        completionCallback = callback
    }
}

public class _SearchableViewController<T: Equatable, Row: SelectableRowType where Row: BaseRow, Row: TypedRowType, Row.Value == T, Row.Cell.Value == T, T: SearchableItem, T: CustomStringConvertible> : UITableViewController, UISearchResultsUpdating, TypedRowControllerType  {
    public var row: RowOf<Row.Value>!
    let searchController = UISearchController(searchResultsController: nil)

    required public init() {
        super.init(style: .Grouped)
        searchController.searchResultsUpdater = self
        searchController.dimsBackgroundDuringPresentation = false
        self.definesPresentationContext = true
    }
    var originalOptions = [T]()
    var currentOptions = [T]()
    public override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        tableView!.tableHeaderView = searchController.searchBar
        if let options = row.dataProvider?.arrayData {
            self.originalOptions = options
            self.currentOptions = options
        }
    }
    private func filter(query: String) {
        if query == "" {
            currentOptions = self.originalOptions
        } else {
            currentOptions = self.originalOptions.filter{ $0.matchesSearchQuery(query) }
        }
        self.tableView.reloadData()
    }

    public override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    public override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return self.row?.title
    }
    public override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return currentOptions.count
    }
    public override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let option = self.currentOptions[indexPath.row]
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
        cell.textLabel?.text = option.description
        return cell
    }
    public override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let option = self.currentOptions[indexPath.row]
        row.value = option
        completionCallback?(self)
    }
    public var completionCallback : ((UIViewController) -> ())?
    public func updateSearchResultsForSearchController(searchController: UISearchController) {
        filter(searchController.searchBar.text!)
    }
}

/// A selector row where the user can pick an option from a pushed view controller
public final class SearchablePushRow<T: Equatable where T: SearchableItem, T: CustomStringConvertible> : _SearchablePushRow<T, PushSelectorCell<T>>, RowType {
    public required init(tag: String?) {
        super.init(tag: tag)
    }
}

public protocol SearchableItem {
    func matchesSearchQuery(query: String) -> Bool
}

The real magic happens with the SearchableItem that you implement in the swift types you intent to make searchable in the PushRow.


struct LocalityNeighborhood : CustomStringConvertible, Equatable, SearchableItem {
    let id: Int
    let name: String

    var description: String {
        return name
    }
    func matchesSearchQuery(query: String) -> Bool {
        return name.containsString(query)
    }
}

func ==(rhs: LocalityNeighborhood, lhs: LocalityNeighborhood) -> Bool {
    return rhs.id == lhs.id
}

Then you just use it in your Form:

form +++ SearchablePushRow<LocalityNeighborhood>("neighborhood")

Luckily you’d end up with a push row that present search results similar to this(without the spanish of-course xD):

Searchable eureka push row