-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Dynamic Faceting widget (#168)
- Loading branch information
1 parent
ea4ac93
commit 52508ca
Showing
14 changed files
with
931 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
Sources/InstantSearch/DynamicFacets/DynamicFacetListTableViewController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
35
Sources/InstantSearchCore/DynamicFacets/AttributedFacets.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
73 changes: 73 additions & 0 deletions
73
Sources/InstantSearchCore/DynamicFacets/Connector/DynamicFacetListConnector+Controller.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
96 changes: 96 additions & 0 deletions
96
Sources/InstantSearchCore/DynamicFacets/Connector/DynamicFacetListConnector.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() } | ||
} | ||
|
||
} |
24 changes: 24 additions & 0 deletions
24
Sources/InstantSearchCore/DynamicFacets/DynamicFacetListController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
|
||
} |
64 changes: 64 additions & 0 deletions
64
Sources/InstantSearchCore/DynamicFacets/DynamicFacetListInteractor+Controller.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
Oops, something went wrong.