Skip to content

Commit

Permalink
feat: Dynamic Faceting widget (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
VladislavFitz committed Jul 13, 2021
1 parent ea4ac93 commit 52508ca
Show file tree
Hide file tree
Showing 14 changed files with 931 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ let package = Package(
targets: ["InstantSearchInsights"])
],
dependencies: [
.package(name: "AlgoliaSearchClient", url: "https://github.com/algolia/algoliasearch-client-swift", from: "8.8.0")
.package(name: "AlgoliaSearchClient", url: "https://github.com/algolia/algoliasearch-client-swift", .branch("feat/dynamic-faceting"))
],
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// DynamicFacetListTableViewController.swift
//
//
// Created by Vladislav Fitc on 16/03/2021.
//

#if !InstantSearchCocoaPods
import InstantSearchCore
#endif
#if canImport(UIKit) && (os(iOS) || os(macOS))
import UIKit

/// Table view controller presenting ordered facets and ordered facet values
/// Each facet and corresponding values are represented as a table view section
public class DynamicFacetListTableViewController: UITableViewController, DynamicFacetListController {

/// List of ordered facets with their attributes
public var orderedFacets: [AttributedFacets]

/// Set of selected facet values per attribute
public var selections: [Attribute: Set<String>]

// MARK: - DynamicFacetListController

public var didSelect: ((Attribute, Facet) -> Void)?

public func setSelections(_ selections: [Attribute: Set<String>]) {
self.selections = selections
tableView.reloadData()
}

public func setOrderedFacets(_ orderedFacets: [AttributedFacets]) {
self.orderedFacets = orderedFacets
tableView.reloadData()
}

/**
- parameters:
- orderedFacets: List of ordered facets with their attributes
- selections: Set of selected facet values per attribute
*/
public init(orderedFacets: [AttributedFacets] = [],
selections: [Attribute: Set<String>] = [:]) {
self.orderedFacets = orderedFacets
self.selections = selections
super.init(style: .plain)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}

// MARK: - UITableViewDataSource

public override func numberOfSections(in tableView: UITableView) -> Int {
return orderedFacets.count
}

public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return orderedFacets[section].facets.count
}

public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
}

// MARK: - UITableViewDelegate

public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return orderedFacets[section].attribute.rawValue
}

public override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let attribute = orderedFacets[indexPath.section].attribute
let facet = orderedFacets[indexPath.section].facets[indexPath.row]
cell.textLabel?.text = facet.description
cell.accessoryType = (selections[attribute]?.contains(facet.value) ?? false) ? .checkmark : .none
}

public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let unit = orderedFacets[indexPath.section]
let facet = unit.facets[indexPath.row]
didSelect?(unit.attribute, facet)
}

}
#endif
35 changes: 35 additions & 0 deletions Sources/InstantSearchCore/DynamicFacets/AttributedFacets.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// AttributedFacets.swift
//
//
// Created by Vladislav Fitc on 17/03/2021.
//

import Foundation

/// List of ordered facets with their attribute.
public struct AttributedFacets: Codable {

/// Facet attribute
public let attribute: Attribute

/// List of ordered facet values
public let facets: [Facet]

enum CodingKeys: String, CodingKey {
case attribute
case facets = "values"
}

/**
- parameters:
- attribute: Facet attribute
- facets: List of ordered facet values
*/
public init(attribute: Attribute,
facets: [Facet] = []) {
self.attribute = attribute
self.facets = facets
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// DynamicFacetListConnector+Controller.swift
//
//
// Created by Vladislav Fitc on 17/06/2021.
//

import Foundation

public extension DynamicFacetListConnector {

/**
- parameters:
- searcher: Searcher that handles your searches
- filterState: FilterState that holds your filters
- interactor: External dynamic facet list interactor
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
- controller: Controller presenting the ordered list of facets and handling the user interaction
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
*/
convenience init<Controller: DynamicFacetListController>(searcher: Searcher,
filterState: FilterState = .init(),
interactor: DynamicFacetListInteractor = .init(),
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:],
controller: Controller) {
self.init(searcher: searcher,
filterState: filterState,
interactor: interactor,
filterGroupForAttribute: filterGroupForAttribute)
connectController(controller)
}

/**
- parameters:
- searcher: Searcher that handles your searches
- filterState: FilterState that holds your filters
- orderedFacets: Ordered list of attributed facets
- selections: Mapping between a facet attribute and a set of selected facet values.
- selectionModeForAttribute: Mapping between a facet attribute and a facet values selection mode. If not provided, the default selection mode is .single.
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
- controller: Controller presenting the ordered list of facets and handling the user interaction
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
*/
convenience init<Controller: DynamicFacetListController>(searcher: Searcher,
filterState: FilterState = .init(),
orderedFacets: [AttributedFacets] = [],
selections: [Attribute: Set<String>] = [:],
selectionModeForAttribute: [Attribute: SelectionMode] = [:],
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:],
controller: Controller) {
let interactor = DynamicFacetListInteractor(orderedFacets: orderedFacets,
selections: selections,
selectionModeForAttribute: selectionModeForAttribute)
self.init(searcher: searcher,
filterState: filterState,
interactor: interactor,
filterGroupForAttribute: filterGroupForAttribute)
connectController(controller)
}

/**
Establishes a connection with a DynamicFacetListController implementation
- parameter controller: Controller presenting the ordered list of facets and handling the user interaction
*/
@discardableResult func connectController<Controller: DynamicFacetListController>(_ controller: Controller) -> DynamicFacetListInteractor.ControllerConnection<Controller> {
let connection = interactor.connectController(controller)
controllerConnections.append(connection)
return connection
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// DynamicFacetListConnector.swift
//
//
// Created by Vladislav Fitc on 17/06/2021.
//

import Foundation

/// Component that displays automatically ordered facets, their ordered values, and lets the user refine the search results by filtering on specific values.

public class DynamicFacetListConnector<Searcher: SearchResultObservable> where Searcher.SearchResult == SearchResponse {

/// Searcher that handles your searches.
public let searcher: Searcher

/// FilterState that holds your filters
public let filterState: FilterState

/// Logic applied to the facets
public let interactor: DynamicFacetListInteractor

/// Connection between interactor and filter state
public let filterStateConnection: Connection

/// Connection between interactor and searcher
public let searcherConnection: Connection

/// Connections between interactor and controllers
public var controllerConnections: [Connection]

/**
- parameters:
- searcher: Searcher that handles your searches
- filterState: FilterState that holds your filters
- interactor: External dynamic facet list interactor
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
*/
public init(searcher: Searcher,
filterState: FilterState = .init(),
interactor: DynamicFacetListInteractor,
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:]) {
self.searcher = searcher
self.filterState = filterState
self.interactor = interactor
self.controllerConnections = []
searcherConnection = interactor.connectSearcher(searcher)
filterStateConnection = interactor.connectFilterState(filterState,
filterGroupForAttribute: filterGroupForAttribute)
}

/**
- parameters:
- searcher: Searcher that handles your searches
- filterState: FilterState that holds your filters
- orderedFacets: Ordered list of attributed facets
- selections: Mapping between a facet attribute and a set of selected facet values
- selectionModeForAttribute: Mapping between a facet attribute and a facet values selection mode. If not provided, the default selection mode is .single.
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
*/
public convenience init(searcher: Searcher,
filterState: FilterState = .init(),
orderedFacets: [AttributedFacets] = [],
selections: [Attribute: Set<String>] = [:],
selectionModeForAttribute: [Attribute: SelectionMode] = [:],
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:]) {
let interactor = DynamicFacetListInteractor(orderedFacets: orderedFacets,
selections: selections,
selectionModeForAttribute: selectionModeForAttribute)
self.init(searcher: searcher,
filterState: filterState,
interactor: interactor,
filterGroupForAttribute: filterGroupForAttribute)
}

}

extension DynamicFacetListConnector: Connection {

public func connect() {
filterStateConnection.connect()
searcherConnection.connect()
controllerConnections.forEach { $0.connect() }
}

public func disconnect() {
filterStateConnection.disconnect()
searcherConnection.disconnect()
controllerConnections.forEach { $0.disconnect() }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// DynamicFacetListController.swift
//
//
// Created by Vladislav Fitc on 04/06/2021.
//

import Foundation

/**
Controller presenting the ordered list of facets and handling the user interaction
*/
public protocol DynamicFacetListController: AnyObject {

/// Update the list of the ordered attributed facets
func setOrderedFacets(_ orderedFacets: [AttributedFacets])

/// Update the facets selections
func setSelections(_ selections: [Attribute: Set<String>])

/// A closure to trigger when user selects a facet
var didSelect: ((Attribute, Facet) -> Void)? { get set }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// DynamicFacetListInteractor+Controller.swift
//
//
// Created by Vladislav Fitc on 16/03/2021.
//

import Foundation

public extension DynamicFacetListInteractor {

/// Connection between a dynamic facet list business logic and a controller
struct ControllerConnection<Controller: DynamicFacetListController>: Connection {

/// Dynamic facet list business logic
public let interactor: DynamicFacetListInteractor

/// Controller presenting the ordered list of facets and handling user interaction
public let controller: Controller

/**
- parameters:
- interactor: Dynamic facets business logic
- controller: Controller presenting the ordered list of facets and handling the user interaction
*/
public init(interactor: DynamicFacetListInteractor,
controller: Controller) {
self.interactor = interactor
self.controller = controller
}

public func connect() {
controller.didSelect = { [weak interactor] attribute, facet in
guard let interactor = interactor else { return }
interactor.toggleSelection(ofFacetValue: facet.value, for: attribute)
}
interactor.onSelectionsChanged.subscribePast(with: controller) { (controller, selections) in
controller.setSelections(selections)
}.onQueue(.main)

interactor.onFacetOrderChanged.subscribePast(with: controller) { controller, orderedFacets in
controller.setOrderedFacets(orderedFacets)
}.onQueue(.main)
}

public func disconnect() {
controller.didSelect = nil
interactor.onSelectionsChanged.cancelSubscription(for: controller)
interactor.onFacetOrderChanged.cancelSubscription(for: controller)
}

}

/**
Establishes a connection with a DynamicFacetListController implementation
- parameter controller: Controller presenting the ordered list of facets and handling the user interaction
*/
@discardableResult func connectController<Controller: DynamicFacetListController>(_ controller: Controller) -> ControllerConnection<Controller> {
let connection = ControllerConnection(interactor: self, controller: controller)
connection.connect()
return connection
}

}
Loading

0 comments on commit 52508ca

Please sign in to comment.