Skip to content

Commit 52508ca

Browse files
feat: Dynamic Faceting widget (#168)
1 parent ea4ac93 commit 52508ca

14 files changed

+931
-1
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ let package = Package(
2323
targets: ["InstantSearchInsights"])
2424
],
2525
dependencies: [
26-
.package(name: "AlgoliaSearchClient", url: "https://github.com/algolia/algoliasearch-client-swift", from: "8.8.0")
26+
.package(name: "AlgoliaSearchClient", url: "https://github.com/algolia/algoliasearch-client-swift", .branch("feat/dynamic-faceting"))
2727
],
2828
targets: [
2929
.target(
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//
2+
// DynamicFacetListTableViewController.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 16/03/2021.
6+
//
7+
8+
#if !InstantSearchCocoaPods
9+
import InstantSearchCore
10+
#endif
11+
#if canImport(UIKit) && (os(iOS) || os(macOS))
12+
import UIKit
13+
14+
/// Table view controller presenting ordered facets and ordered facet values
15+
/// Each facet and corresponding values are represented as a table view section
16+
public class DynamicFacetListTableViewController: UITableViewController, DynamicFacetListController {
17+
18+
/// List of ordered facets with their attributes
19+
public var orderedFacets: [AttributedFacets]
20+
21+
/// Set of selected facet values per attribute
22+
public var selections: [Attribute: Set<String>]
23+
24+
// MARK: - DynamicFacetListController
25+
26+
public var didSelect: ((Attribute, Facet) -> Void)?
27+
28+
public func setSelections(_ selections: [Attribute: Set<String>]) {
29+
self.selections = selections
30+
tableView.reloadData()
31+
}
32+
33+
public func setOrderedFacets(_ orderedFacets: [AttributedFacets]) {
34+
self.orderedFacets = orderedFacets
35+
tableView.reloadData()
36+
}
37+
38+
/**
39+
- parameters:
40+
- orderedFacets: List of ordered facets with their attributes
41+
- selections: Set of selected facet values per attribute
42+
*/
43+
public init(orderedFacets: [AttributedFacets] = [],
44+
selections: [Attribute: Set<String>] = [:]) {
45+
self.orderedFacets = orderedFacets
46+
self.selections = selections
47+
super.init(style: .plain)
48+
}
49+
50+
required init?(coder: NSCoder) {
51+
fatalError("init(coder:) has not been implemented")
52+
}
53+
54+
public override func viewDidLoad() {
55+
super.viewDidLoad()
56+
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
57+
}
58+
59+
// MARK: - UITableViewDataSource
60+
61+
public override func numberOfSections(in tableView: UITableView) -> Int {
62+
return orderedFacets.count
63+
}
64+
65+
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
66+
return orderedFacets[section].facets.count
67+
}
68+
69+
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
70+
return tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
71+
}
72+
73+
// MARK: - UITableViewDelegate
74+
75+
public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
76+
return orderedFacets[section].attribute.rawValue
77+
}
78+
79+
public override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
80+
let attribute = orderedFacets[indexPath.section].attribute
81+
let facet = orderedFacets[indexPath.section].facets[indexPath.row]
82+
cell.textLabel?.text = facet.description
83+
cell.accessoryType = (selections[attribute]?.contains(facet.value) ?? false) ? .checkmark : .none
84+
}
85+
86+
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
87+
let unit = orderedFacets[indexPath.section]
88+
let facet = unit.facets[indexPath.row]
89+
didSelect?(unit.attribute, facet)
90+
}
91+
92+
}
93+
#endif
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// AttributedFacets.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 17/03/2021.
6+
//
7+
8+
import Foundation
9+
10+
/// List of ordered facets with their attribute.
11+
public struct AttributedFacets: Codable {
12+
13+
/// Facet attribute
14+
public let attribute: Attribute
15+
16+
/// List of ordered facet values
17+
public let facets: [Facet]
18+
19+
enum CodingKeys: String, CodingKey {
20+
case attribute
21+
case facets = "values"
22+
}
23+
24+
/**
25+
- parameters:
26+
- attribute: Facet attribute
27+
- facets: List of ordered facet values
28+
*/
29+
public init(attribute: Attribute,
30+
facets: [Facet] = []) {
31+
self.attribute = attribute
32+
self.facets = facets
33+
}
34+
35+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// DynamicFacetListConnector+Controller.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 17/06/2021.
6+
//
7+
8+
import Foundation
9+
10+
public extension DynamicFacetListConnector {
11+
12+
/**
13+
- parameters:
14+
- searcher: Searcher that handles your searches
15+
- filterState: FilterState that holds your filters
16+
- interactor: External dynamic facet list interactor
17+
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
18+
- controller: Controller presenting the ordered list of facets and handling the user interaction
19+
20+
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
21+
*/
22+
convenience init<Controller: DynamicFacetListController>(searcher: Searcher,
23+
filterState: FilterState = .init(),
24+
interactor: DynamicFacetListInteractor = .init(),
25+
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:],
26+
controller: Controller) {
27+
self.init(searcher: searcher,
28+
filterState: filterState,
29+
interactor: interactor,
30+
filterGroupForAttribute: filterGroupForAttribute)
31+
connectController(controller)
32+
}
33+
34+
/**
35+
- parameters:
36+
- searcher: Searcher that handles your searches
37+
- filterState: FilterState that holds your filters
38+
- orderedFacets: Ordered list of attributed facets
39+
- selections: Mapping between a facet attribute and a set of selected facet values.
40+
- selectionModeForAttribute: Mapping between a facet attribute and a facet values selection mode. If not provided, the default selection mode is .single.
41+
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
42+
- controller: Controller presenting the ordered list of facets and handling the user interaction
43+
44+
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
45+
*/
46+
convenience init<Controller: DynamicFacetListController>(searcher: Searcher,
47+
filterState: FilterState = .init(),
48+
orderedFacets: [AttributedFacets] = [],
49+
selections: [Attribute: Set<String>] = [:],
50+
selectionModeForAttribute: [Attribute: SelectionMode] = [:],
51+
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:],
52+
controller: Controller) {
53+
let interactor = DynamicFacetListInteractor(orderedFacets: orderedFacets,
54+
selections: selections,
55+
selectionModeForAttribute: selectionModeForAttribute)
56+
self.init(searcher: searcher,
57+
filterState: filterState,
58+
interactor: interactor,
59+
filterGroupForAttribute: filterGroupForAttribute)
60+
connectController(controller)
61+
}
62+
63+
/**
64+
Establishes a connection with a DynamicFacetListController implementation
65+
- parameter controller: Controller presenting the ordered list of facets and handling the user interaction
66+
*/
67+
@discardableResult func connectController<Controller: DynamicFacetListController>(_ controller: Controller) -> DynamicFacetListInteractor.ControllerConnection<Controller> {
68+
let connection = interactor.connectController(controller)
69+
controllerConnections.append(connection)
70+
return connection
71+
}
72+
73+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// DynamicFacetListConnector.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 17/06/2021.
6+
//
7+
8+
import Foundation
9+
10+
/// Component that displays automatically ordered facets, their ordered values, and lets the user refine the search results by filtering on specific values.
11+
12+
public class DynamicFacetListConnector<Searcher: SearchResultObservable> where Searcher.SearchResult == SearchResponse {
13+
14+
/// Searcher that handles your searches.
15+
public let searcher: Searcher
16+
17+
/// FilterState that holds your filters
18+
public let filterState: FilterState
19+
20+
/// Logic applied to the facets
21+
public let interactor: DynamicFacetListInteractor
22+
23+
/// Connection between interactor and filter state
24+
public let filterStateConnection: Connection
25+
26+
/// Connection between interactor and searcher
27+
public let searcherConnection: Connection
28+
29+
/// Connections between interactor and controllers
30+
public var controllerConnections: [Connection]
31+
32+
/**
33+
- parameters:
34+
- searcher: Searcher that handles your searches
35+
- filterState: FilterState that holds your filters
36+
- interactor: External dynamic facet list interactor
37+
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
38+
39+
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
40+
*/
41+
public init(searcher: Searcher,
42+
filterState: FilterState = .init(),
43+
interactor: DynamicFacetListInteractor,
44+
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:]) {
45+
self.searcher = searcher
46+
self.filterState = filterState
47+
self.interactor = interactor
48+
self.controllerConnections = []
49+
searcherConnection = interactor.connectSearcher(searcher)
50+
filterStateConnection = interactor.connectFilterState(filterState,
51+
filterGroupForAttribute: filterGroupForAttribute)
52+
}
53+
54+
/**
55+
- parameters:
56+
- searcher: Searcher that handles your searches
57+
- filterState: FilterState that holds your filters
58+
- orderedFacets: Ordered list of attributed facets
59+
- selections: Mapping between a facet attribute and a set of selected facet values
60+
- selectionModeForAttribute: Mapping between a facet attribute and a facet values selection mode. If not provided, the default selection mode is .single.
61+
- filterGroupForAttribute: Mapping between a facet attribute and a descriptor of a filter group where the corresponding facet filters stored in the filter state.
62+
63+
If no filter group descriptor provided, the filters for attribute will be automatically stored in the conjunctive (`and`) group with the facet attribute name.
64+
*/
65+
public convenience init(searcher: Searcher,
66+
filterState: FilterState = .init(),
67+
orderedFacets: [AttributedFacets] = [],
68+
selections: [Attribute: Set<String>] = [:],
69+
selectionModeForAttribute: [Attribute: SelectionMode] = [:],
70+
filterGroupForAttribute: [Attribute: FilterGroupDescriptor] = [:]) {
71+
let interactor = DynamicFacetListInteractor(orderedFacets: orderedFacets,
72+
selections: selections,
73+
selectionModeForAttribute: selectionModeForAttribute)
74+
self.init(searcher: searcher,
75+
filterState: filterState,
76+
interactor: interactor,
77+
filterGroupForAttribute: filterGroupForAttribute)
78+
}
79+
80+
}
81+
82+
extension DynamicFacetListConnector: Connection {
83+
84+
public func connect() {
85+
filterStateConnection.connect()
86+
searcherConnection.connect()
87+
controllerConnections.forEach { $0.connect() }
88+
}
89+
90+
public func disconnect() {
91+
filterStateConnection.disconnect()
92+
searcherConnection.disconnect()
93+
controllerConnections.forEach { $0.disconnect() }
94+
}
95+
96+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// DynamicFacetListController.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 04/06/2021.
6+
//
7+
8+
import Foundation
9+
10+
/**
11+
Controller presenting the ordered list of facets and handling the user interaction
12+
*/
13+
public protocol DynamicFacetListController: AnyObject {
14+
15+
/// Update the list of the ordered attributed facets
16+
func setOrderedFacets(_ orderedFacets: [AttributedFacets])
17+
18+
/// Update the facets selections
19+
func setSelections(_ selections: [Attribute: Set<String>])
20+
21+
/// A closure to trigger when user selects a facet
22+
var didSelect: ((Attribute, Facet) -> Void)? { get set }
23+
24+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// DynamicFacetListInteractor+Controller.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 16/03/2021.
6+
//
7+
8+
import Foundation
9+
10+
public extension DynamicFacetListInteractor {
11+
12+
/// Connection between a dynamic facet list business logic and a controller
13+
struct ControllerConnection<Controller: DynamicFacetListController>: Connection {
14+
15+
/// Dynamic facet list business logic
16+
public let interactor: DynamicFacetListInteractor
17+
18+
/// Controller presenting the ordered list of facets and handling user interaction
19+
public let controller: Controller
20+
21+
/**
22+
- parameters:
23+
- interactor: Dynamic facets business logic
24+
- controller: Controller presenting the ordered list of facets and handling the user interaction
25+
*/
26+
public init(interactor: DynamicFacetListInteractor,
27+
controller: Controller) {
28+
self.interactor = interactor
29+
self.controller = controller
30+
}
31+
32+
public func connect() {
33+
controller.didSelect = { [weak interactor] attribute, facet in
34+
guard let interactor = interactor else { return }
35+
interactor.toggleSelection(ofFacetValue: facet.value, for: attribute)
36+
}
37+
interactor.onSelectionsChanged.subscribePast(with: controller) { (controller, selections) in
38+
controller.setSelections(selections)
39+
}.onQueue(.main)
40+
41+
interactor.onFacetOrderChanged.subscribePast(with: controller) { controller, orderedFacets in
42+
controller.setOrderedFacets(orderedFacets)
43+
}.onQueue(.main)
44+
}
45+
46+
public func disconnect() {
47+
controller.didSelect = nil
48+
interactor.onSelectionsChanged.cancelSubscription(for: controller)
49+
interactor.onFacetOrderChanged.cancelSubscription(for: controller)
50+
}
51+
52+
}
53+
54+
/**
55+
Establishes a connection with a DynamicFacetListController implementation
56+
- parameter controller: Controller presenting the ordered list of facets and handling the user interaction
57+
*/
58+
@discardableResult func connectController<Controller: DynamicFacetListController>(_ controller: Controller) -> ControllerConnection<Controller> {
59+
let connection = ControllerConnection(interactor: self, controller: controller)
60+
connection.connect()
61+
return connection
62+
}
63+
64+
}

0 commit comments

Comments
 (0)