Skip to content

Commit a189b0c

Browse files
feat(SwiftUI): Full-fledged SwiftUI support (#178)
SwiftUI support: - Loading - FilterToggle - ClearFilters - Stats - QueryRuleCustomData - SortBy - RelevantSort - FilterNumericRange - CurrentFilters - FiltersList Implementation improvement: - SearchBar - Hits - FacetList - HierarchicalList
1 parent 74c36d1 commit a189b0c

File tree

43 files changed

+780
-196
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+780
-196
lines changed

Sources/InstantSearch/SwiftUI/FacetList.swift

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,14 @@ public struct FacetList<Row: View, NoResults: View>: View {
3434
if let noResults = noResults?(), facetListObservableController.facets.isEmpty {
3535
noResults
3636
} else {
37-
ScrollView(showsIndicators: true) {
38-
VStack {
39-
ForEach(facetListObservableController.facets, id: \.self) { facet in
40-
row(facet, facetListObservableController.isSelected(facet))
41-
.onTapGesture {
42-
facetListObservableController.toggle(facet)
43-
}
44-
}
37+
VStack {
38+
ForEach(facetListObservableController.facets, id: \.self) { facet in
39+
row(facet, facetListObservableController.isSelected(facet))
40+
.onTapGesture {
41+
facetListObservableController.toggle(facet)
42+
}
4543
}
4644
}
47-
4845
}
4946
}
5047

@@ -86,17 +83,17 @@ struct Facets_Previews: PreviewProvider {
8683
}
8784
}()
8885

89-
static let controller: FacetListObservableController = {
90-
let controller = FacetListObservableController(facets: test, selections: ["Samsung"])
91-
controller.onClick = { facet in
92-
controller.selections.formSymmetricDifference([facet.value])
86+
static let demoController: FacetListObservableController = {
87+
let demoController = FacetListObservableController(facets: test, selections: ["Samsung"])
88+
demoController.onClick = { facet in
89+
demoController.selections.formSymmetricDifference([facet.value])
9390
}
94-
return controller
91+
return demoController
9592
}()
9693

9794
static var previews: some View {
9895
NavigationView {
99-
FacetList(controller, row: FacetRow.init)
96+
FacetList(demoController, row: FacetRow.init)
10097
}
10198
}
10299

Sources/InstantSearch/SwiftUI/FacetRow.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public struct FacetRow: View {
3535
Spacer()
3636
if isSelected {
3737
Image(systemName: "checkmark")
38-
.frame(maxHeight: .infinity, alignment: .trailing)
3938
.foregroundColor(.accentColor)
4039
}
4140
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// FilterList.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 21/06/2021.
6+
//
7+
8+
import Foundation
9+
#if canImport(Combine) && canImport(SwiftUI) && (os(iOS) || os(macOS))
10+
import Combine
11+
import SwiftUI
12+
13+
/// A view presenting the list of filters
14+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
15+
public struct FilterList<Filter: FilterType & Hashable, Row: View, NoResults: View>: View {
16+
17+
@ObservedObject public var filtersListObservableController: FilterListObservableController<Filter>
18+
19+
/// Closure constructing a filter row view
20+
public var row: (Filter, Bool) -> Row
21+
22+
/// Closure constructing a no results view
23+
public var noResults: (() -> NoResults)?
24+
25+
public init(_ filtersListObservableController: FilterListObservableController<Filter>,
26+
@ViewBuilder row: @escaping (Filter, Bool) -> Row,
27+
@ViewBuilder noResults: @escaping () -> NoResults) {
28+
self.filtersListObservableController = filtersListObservableController
29+
self.row = row
30+
self.noResults = noResults
31+
}
32+
33+
public var body: some View {
34+
if let noResults = noResults?(), filtersListObservableController.filters.isEmpty {
35+
noResults
36+
} else {
37+
VStack {
38+
ForEach(filtersListObservableController.filters, id: \.self) { filter in
39+
row(filter, filtersListObservableController.isSelected(filter))
40+
.onTapGesture {
41+
filtersListObservableController.toggle(filter)
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
}
49+
50+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
51+
public extension FilterList where NoResults == Never {
52+
53+
init(_ filtersListObservableController: FilterListObservableController<Filter>,
54+
@ViewBuilder row: @escaping(Filter, Bool) -> Row) {
55+
self.filtersListObservableController = filtersListObservableController
56+
self.row = row
57+
self.noResults = nil
58+
}
59+
60+
}
61+
62+
#endif
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// HierarchicalFacetRow.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 29/06/2021.
6+
//
7+
8+
import Foundation
9+
#if canImport(Combine) && canImport(SwiftUI)
10+
import Combine
11+
import SwiftUI
12+
13+
@available(iOS 13.0, OSX 11.00, tvOS 13.0, watchOS 6.0, *)
14+
public struct HierarchicalFacetRow: View {
15+
16+
/// Facet value
17+
public var facet: Facet
18+
19+
/// Facet selection state
20+
public var isSelected: Bool
21+
22+
/// Facet nesting level in the hierarchy
23+
public var nestingLevel: Int
24+
25+
/// Character separating the facets in the hierarchical facet
26+
///
27+
/// Default value: ">"
28+
public var separator: Character
29+
30+
public var body: some View {
31+
HStack(spacing: 10) {
32+
Image(systemName: isSelected ? "chevron.down" : "chevron.right")
33+
.font(.callout)
34+
let displayFacetValue = facet
35+
.value
36+
.split(separator: separator)
37+
.map { $0.trimmingCharacters(in: .whitespaces) }.last ?? ""
38+
Text("\(displayFacetValue) (\(facet.count))")
39+
.fontWeight(isSelected ? .semibold : .regular)
40+
.contentShape(Rectangle())
41+
Spacer()
42+
}
43+
.padding(.leading, CGFloat(nestingLevel * 20))
44+
}
45+
46+
public init(facet: Facet,
47+
nestingLevel: Int,
48+
isSelected: Bool,
49+
separator: Character = ">") {
50+
self.facet = facet
51+
self.nestingLevel = nestingLevel
52+
self.isSelected = isSelected
53+
self.separator = separator
54+
}
55+
56+
}
57+
#endif

Sources/InstantSearch/SwiftUI/HierarchicalList.swift

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,50 @@ import SwiftUI
1212

1313
/// A view presenting the list of hierarchical facets
1414
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
15-
public struct HierarchicalList: View {
15+
public struct HierarchicalList<Row: View, NoResults: View>: View {
1616

17-
@ObservedObject var hierarchicalController: HierarchicalObservableController
17+
@ObservedObject public var hierarchicalObservableController: HierarchicalObservableController
18+
19+
/// Closure constructing a hierarchical facet row view
20+
public var row: (Facet, Int, Bool) -> Row
21+
22+
/// Closure constructing a no results view
23+
public var noResults: (() -> NoResults)?
24+
25+
public init(_ hierarchicalObservableController: HierarchicalObservableController,
26+
@ViewBuilder row: @escaping (Facet, Int, Bool) -> Row,
27+
@ViewBuilder noResults: @escaping () -> NoResults) {
28+
self.hierarchicalObservableController = hierarchicalObservableController
29+
self.row = row
30+
self.noResults = noResults
31+
}
1832

1933
public var body: some View {
20-
VStack(alignment: .leading, spacing: 5) {
21-
ForEach(hierarchicalController.items.prefix(20), id: \.facet) { item in
22-
let (_, level, isSelected) = item
23-
let facet = self.facet(from: item)
24-
HStack(spacing: 10) {
25-
Image(systemName: isSelected ? "chevron.down" : "chevron.right")
26-
.font(.callout)
27-
Text("\(facet.value) (\(facet.count))")
28-
.fontWeight(isSelected ? .semibold : .regular)
29-
}
30-
.padding(.leading, CGFloat(level * 15))
31-
.onTapGesture {
32-
hierarchicalController.toggle(item.facet.value)
34+
if let noResults = noResults?(), hierarchicalObservableController.hierarchicalFacets.isEmpty {
35+
noResults
36+
} else {
37+
VStack {
38+
ForEach(hierarchicalObservableController.hierarchicalFacets, id: \.facet) { hierarchicalFacet in
39+
let (facet, level, isSelected) = hierarchicalFacet
40+
row(facet, level, isSelected)
41+
.onTapGesture {
42+
hierarchicalObservableController.toggle(facet.value)
43+
}
3344
}
3445
}
3546
}
3647
}
3748

38-
private func maxSelectedLevel(_ hierarchicalFacets: [HierarchicalFacet]) -> Int? {
39-
return hierarchicalFacets
40-
.filter { $0.isSelected }
41-
.max { $0.level < $1.level }?
42-
.level
43-
}
49+
}
4450

45-
private func facet(from hierarchicalFacet: HierarchicalFacet) -> Facet {
46-
let value = hierarchicalFacet
47-
.facet
48-
.value
49-
.split(separator: ">")
50-
.map { $0.trimmingCharacters(in: .whitespaces) }[hierarchicalFacet.level]
51-
return Facet(value: value, count: hierarchicalFacet.facet.count, highlighted: nil)
52-
}
51+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
52+
public extension HierarchicalList where NoResults == Never {
5353

54-
public init(hierarchicalController: HierarchicalObservableController) {
55-
self.hierarchicalController = hierarchicalController
54+
init(_ hierarchicalObservableController: HierarchicalObservableController,
55+
@ViewBuilder row: @escaping(Facet, Int, Bool) -> Row) {
56+
self.hierarchicalObservableController = hierarchicalObservableController
57+
self.row = row
58+
self.noResults = nil
5659
}
5760

5861
}
@@ -61,23 +64,28 @@ public struct HierarchicalList: View {
6164
struct HierarchicalListPreview: PreviewProvider {
6265

6366
static var previews: some View {
64-
let controller: HierarchicalObservableController = .init()
65-
HierarchicalList(hierarchicalController: controller)
66-
.onAppear {
67-
controller.setItem([
68-
(Facet(value: "Category1", count: 10), 0, false),
69-
(Facet(value: "Category1 > Category1-1", count: 7), 1, false),
70-
(Facet(value: "Category1 > Category1-2", count: 2), 1, false),
71-
(Facet(value: "Category1 > Category1-3", count: 1), 1, false),
72-
(Facet(value: "Category2", count: 14), 0, true),
73-
(Facet(value: "Category2 > Category2-1", count: 8), 1, false),
74-
(Facet(value: "Category2 > Category2-2", count: 4), 1, true),
75-
(Facet(value: "Category2 > Category2-2 > Category2-2-1", count: 2), 2, false),
76-
(Facet(value: "Category2 > Category2-2 > Category2-2-2", count: 2), 2, true),
77-
(Facet(value: "Category2 > Category2-3", count: 2), 1, false)
78-
])
79-
}
67+
let demoController: HierarchicalObservableController = .init()
68+
HierarchicalList(demoController) { facet, nestingLevel, isSelected in
69+
HierarchicalFacetRow(facet: facet,
70+
nestingLevel: nestingLevel,
71+
isSelected: isSelected)
72+
}
73+
.onAppear {
74+
demoController.setItem([
75+
(Facet(value: "Category1", count: 10), 0, false),
76+
(Facet(value: "Category1 > Category1-1", count: 7), 1, false),
77+
(Facet(value: "Category1 > Category1-2", count: 2), 1, false),
78+
(Facet(value: "Category1 > Category1-3", count: 1), 1, false),
79+
(Facet(value: "Category2", count: 14), 0, true),
80+
(Facet(value: "Category2 > Category2-1", count: 8), 1, false),
81+
(Facet(value: "Category2 > Category2-2", count: 4), 1, true),
82+
(Facet(value: "Category2 > Category2-2 > Category2-2-1", count: 2), 2, false),
83+
(Facet(value: "Category2 > Category2-2 > Category2-2-2", count: 2), 2, true),
84+
(Facet(value: "Category2 > Category2-3", count: 2), 1, false)
85+
])
86+
}
8087
}
8188

8289
}
90+
8391
#endif
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// SuggestionRow.swift
3+
//
4+
//
5+
// Created by Vladislav Fitc on 01/04/2021.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
/// A view presenting a search query suggestion
12+
@available(iOS 13.0, OSX 11.0, tvOS 13.0, watchOS 6.0, *)
13+
public struct SuggestionRow: View {
14+
15+
/// Suggestion
16+
public let suggestion: QuerySuggestion
17+
18+
/// An action triggered when typeahead button (arrow) tapped
19+
public var onTypeAhead: (String) -> Void
20+
21+
/// An action triggered when suggestion selected
22+
public var onSelection: (String) -> Void
23+
24+
private func valueText(for suggestion: QuerySuggestion) -> Text {
25+
if let highlightedValue = suggestion.highlighted {
26+
let highlightedValueString = HighlightedString(string: highlightedValue)
27+
return Text(highlightedString: highlightedValueString) { Text($0).bold() }
28+
} else {
29+
return Text(suggestion.query)
30+
}
31+
}
32+
33+
public init(suggestion: QuerySuggestion,
34+
onSelection: @escaping (String) -> Void,
35+
onTypeAhead: @escaping (String) -> Void) {
36+
self.suggestion = suggestion
37+
self.onSelection = onSelection
38+
self.onTypeAhead = onTypeAhead
39+
}
40+
41+
public var body: some View {
42+
let stack =
43+
HStack {
44+
valueText(for: suggestion)
45+
.padding(.vertical, 3)
46+
Spacer()
47+
Button(action: {
48+
onTypeAhead(suggestion.query)
49+
},
50+
label: {
51+
Image(systemName: "arrow.up.backward")
52+
.foregroundColor(.gray)
53+
})
54+
}
55+
.padding(.vertical, 4)
56+
.padding(.horizontal, 20)
57+
.contentShape(Rectangle())
58+
#if os(tvOS)
59+
return stack
60+
#else
61+
return stack
62+
.onTapGesture {
63+
onSelection(suggestion.query)
64+
}
65+
#endif
66+
}
67+
68+
}

Sources/InstantSearchCore/Common/Item/ItemController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
public protocol ItemController: class {
11+
public protocol ItemController: AnyObject {
1212

1313
associatedtype Item
1414

Sources/InstantSearchCore/Common/Number/Boundable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
public protocol Boundable: class {
11+
public protocol Boundable: AnyObject {
1212
associatedtype Number: Comparable & DoubleRepresentable
1313

1414
func applyBounds(bounds: ClosedRange<Number>?)

0 commit comments

Comments
 (0)