From f5cdd678d877a5e35c55872337801997dfa79cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Tue, 10 Jan 2023 23:22:04 +0100 Subject: [PATCH 1/7] Feat: add clear support to HitsSource --- .../Hits/HitsInteractor.swift | 5 +++- .../InstantSearchCore/Hits/HitsSource.swift | 2 +- .../Pagination/Paginator.swift | 3 +++ .../DataModel/HitsObservableController.swift | 10 +++++-- .../Unit/Hits/HitsInteractorTests.swift | 26 +++++++++++++++++++ .../Unit/PaginatorTests.swift | 10 +++++++ Tests/InstantSearchTests/TestHitsSource.swift | 5 +++- 7 files changed, 56 insertions(+), 5 deletions(-) diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor.swift b/Sources/InstantSearchCore/Hits/HitsInteractor.swift index fb5fd0ef..e01bf700 100644 --- a/Sources/InstantSearchCore/Hits/HitsInteractor.swift +++ b/Sources/InstantSearchCore/Hits/HitsInteractor.swift @@ -154,7 +154,10 @@ public class HitsInteractor: AnyHitsInteractor { infiniteScrollingController.calculatePagesAndLoad(currentRow: rowIndex, offset: pageLoadOffset, pageMap: hitsPageMap) } - + + public func clear() { + paginator.clear() + } } extension HitsInteractor { diff --git a/Sources/InstantSearchCore/Hits/HitsSource.swift b/Sources/InstantSearchCore/Hits/HitsSource.swift index d9619169..8710a89f 100644 --- a/Sources/InstantSearchCore/Hits/HitsSource.swift +++ b/Sources/InstantSearchCore/Hits/HitsSource.swift @@ -14,7 +14,7 @@ public protocol HitsSource: AnyObject { func numberOfHits() -> Int func hit(atIndex index: Int) -> Record? - + func clear() } extension HitsInteractor: HitsSource {} diff --git a/Sources/InstantSearchCore/Pagination/Paginator.swift b/Sources/InstantSearchCore/Pagination/Paginator.swift index a2adb74b..16a6dd07 100644 --- a/Sources/InstantSearchCore/Pagination/Paginator.swift +++ b/Sources/InstantSearchCore/Pagination/Paginator.swift @@ -39,4 +39,7 @@ class Paginator { isInvalidated = true } + public func clear() { + pageMap = nil + } } diff --git a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift index bc1e377f..5671aa0f 100644 --- a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift +++ b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift @@ -16,9 +16,15 @@ import SwiftUI /// HitsController implementation adapted for usage with SwiftUI views @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public class HitsObservableController: ObservableObject, HitsController { - + /// List of hits itemsto present - @Published public var hits: [Hit?] + @Published public var hits: [Hit?] { + didSet { + if hits.isEmpty { + hitsSource?.clear() + } + } + } /// The state ID to assign to the scrollview presenting the hits @Published public var scrollID: UUID diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift index 89574129..73f43501 100644 --- a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift @@ -235,5 +235,31 @@ class HitsInteractorTests: XCTestCase { waitForExpectations(timeout: 5) } + + func testClearTriggering() throws { + let paginationController = Paginator>() + let infiniteScrollingController = TestInfiniteScrollingController() + + let hits = (0..<20).map(TestRecord.withValue) + let results = SearchResponse(hits: hits) + + let vm = HitsInteractor( + settings: .init(showItemsOnEmptyQuery: true), + paginationController: paginationController, + infiniteScrollingController: infiniteScrollingController + ) + + let exp = expectation(description: "on results updated") + + vm.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(vm.numberOfHits(), hits.count) + exp.fulfill() + } + vm.update(results) + waitForExpectations(timeout: 3, handler: .none) + + vm.clear() + XCTAssertEqual(vm.numberOfHits(), 0) + } } diff --git a/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift b/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift index 0ada0bd3..006337e7 100644 --- a/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift +++ b/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift @@ -93,5 +93,15 @@ class PaginatorTests: XCTestCase { XCTAssertEqual(paginator.pageMap?.count, 3) } + + func testClear() { + let paginator = Paginator() + let page = TestPageable(index: 0, items: ["i1", "i2", "i3"]) + paginator.process(page) + XCTAssertEqual(paginator.pageMap?.count, 3) + + paginator.clear() + XCTAssertNil(paginator.pageMap) + } } diff --git a/Tests/InstantSearchTests/TestHitsSource.swift b/Tests/InstantSearchTests/TestHitsSource.swift index a99a1e0e..e22f3bf1 100644 --- a/Tests/InstantSearchTests/TestHitsSource.swift +++ b/Tests/InstantSearchTests/TestHitsSource.swift @@ -13,7 +13,7 @@ class TestHitsSource: HitsSource { typealias Hit = String - let hits: [String] + var hits: [String] init(hits: [String]) { self.hits = hits @@ -28,4 +28,7 @@ class TestHitsSource: HitsSource { return hits[index] } + func clear() { + hits = [] + } } From d7234b54474465b2a0277eabd4f4847c80b07946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Sun, 15 Jan 2023 15:43:47 +0100 Subject: [PATCH 2/7] Apply suggestions from code review Co-authored-by: Vladislav Fitc --- Sources/InstantSearchCore/Hits/HitsSource.swift | 1 - .../DataModel/HitsObservableController.swift | 7 ------- 2 files changed, 8 deletions(-) diff --git a/Sources/InstantSearchCore/Hits/HitsSource.swift b/Sources/InstantSearchCore/Hits/HitsSource.swift index 8710a89f..b697feb5 100644 --- a/Sources/InstantSearchCore/Hits/HitsSource.swift +++ b/Sources/InstantSearchCore/Hits/HitsSource.swift @@ -14,7 +14,6 @@ public protocol HitsSource: AnyObject { func numberOfHits() -> Int func hit(atIndex index: Int) -> Record? - func clear() } extension HitsInteractor: HitsSource {} diff --git a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift index 5671aa0f..8aba885b 100644 --- a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift +++ b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift @@ -16,15 +16,8 @@ import SwiftUI /// HitsController implementation adapted for usage with SwiftUI views @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public class HitsObservableController: ObservableObject, HitsController { - /// List of hits itemsto present @Published public var hits: [Hit?] { - didSet { - if hits.isEmpty { - hitsSource?.clear() - } - } - } /// The state ID to assign to the scrollview presenting the hits @Published public var scrollID: UUID From 1f2ecf374cd02af85bde4aba6c3776b60d5538d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Sun, 15 Jan 2023 15:49:09 +0100 Subject: [PATCH 3/7] Refactor: remove clear method from `HitsSource` Also mark `HitsObservableController` @Published props as private(set) --- Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift | 1 + Sources/InstantSearchCore/Hits/HitsSource.swift | 1 + .../DataModel/HitsObservableController.swift | 7 ++++--- .../Unit/MultiIndexHitsInteractorTests.swift | 4 ++++ Tests/InstantSearchTests/TestHitsSource.swift | 5 +---- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift index c2f3d4b5..36bf94c6 100644 --- a/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift +++ b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift @@ -48,4 +48,5 @@ public protocol AnyHitsInteractor: AnyObject { func notifyQueryChanged() func process(_ error: Swift.Error, for query: Query) + func clear() } diff --git a/Sources/InstantSearchCore/Hits/HitsSource.swift b/Sources/InstantSearchCore/Hits/HitsSource.swift index b697feb5..d9619169 100644 --- a/Sources/InstantSearchCore/Hits/HitsSource.swift +++ b/Sources/InstantSearchCore/Hits/HitsSource.swift @@ -14,6 +14,7 @@ public protocol HitsSource: AnyObject { func numberOfHits() -> Int func hit(atIndex index: Int) -> Record? + } extension HitsInteractor: HitsSource {} diff --git a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift index 8aba885b..29cc8e9b 100644 --- a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift +++ b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift @@ -16,11 +16,12 @@ import SwiftUI /// HitsController implementation adapted for usage with SwiftUI views @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public class HitsObservableController: ObservableObject, HitsController { - /// List of hits itemsto present - @Published public var hits: [Hit?] { + + /// List of hits items to present + @Published private(set) public var hits: [Hit?] /// The state ID to assign to the scrollview presenting the hits - @Published public var scrollID: UUID + @Published private(set) public var scrollID: UUID public var hitsSource: HitsInteractor? diff --git a/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift index b291c250..e1751260 100644 --- a/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift +++ b/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift @@ -383,6 +383,10 @@ class MultiIndexHitsInteractorTests: XCTestCase { func loadMoreResults() { didCallLoadMoreResults() } + + func clear() { + + } } } diff --git a/Tests/InstantSearchTests/TestHitsSource.swift b/Tests/InstantSearchTests/TestHitsSource.swift index e22f3bf1..a99a1e0e 100644 --- a/Tests/InstantSearchTests/TestHitsSource.swift +++ b/Tests/InstantSearchTests/TestHitsSource.swift @@ -13,7 +13,7 @@ class TestHitsSource: HitsSource { typealias Hit = String - var hits: [String] + let hits: [String] init(hits: [String]) { self.hits = hits @@ -28,7 +28,4 @@ class TestHitsSource: HitsSource { return hits[index] } - func clear() { - hits = [] - } } From 3c359d4c312aac8746dc1de8ade6d55218057aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Mon, 23 Jan 2023 09:01:51 +0100 Subject: [PATCH 4/7] Fix: HitsList SwiftUI Previews Fix build issue due to private(set) accessor of `hits` in HitsObservableController --- .../InstantSearchSwiftUI/View/HitsList.swift | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/Sources/InstantSearchSwiftUI/View/HitsList.swift b/Sources/InstantSearchSwiftUI/View/HitsList.swift index 3d346c8f..2ca40a43 100644 --- a/Sources/InstantSearchSwiftUI/View/HitsList.swift +++ b/Sources/InstantSearchSwiftUI/View/HitsList.swift @@ -75,14 +75,46 @@ public extension HitsList where NoResults == Never { #if os(iOS) @available(iOS 13.0, tvOS 13.0, watchOS 7.0, *) struct HitsView_Previews: PreviewProvider { + + struct PreviewRecord: Codable { + let objectID: ObjectID + let value: Value + + init(_ value: Value, objectID: ObjectID = ObjectID(rawValue: UUID().uuidString)) { + self.value = value + self.objectID = objectID + } + + static func withValue(_ value: Value) -> Self { + .init(value) + } + } + + static let rawHits: Data = """ + { + "hits": [ + { + "objectID": "1", + "value": "h1" + }, + { + "objectID": "2", + "value": "h2" + } + ] + } + """.data(using: .utf8)! + + static let hitsController: HitsObservableController> = .init() + + static let interactor = HitsInteractor>(infiniteScrolling: .off, showItemsOnEmptyQuery: true) static var previews: some View { - let hitsController: HitsObservableController = .init() NavigationView { HitsList(hitsController) { string, _ in VStack { HStack { - Text(string ?? "---") + Text(string?.value ?? "---") .frame(maxWidth: .infinity, minHeight: 30, maxHeight: .infinity, alignment: .leading) .padding(.horizontal, 16) } @@ -93,14 +125,22 @@ struct HitsView_Previews: PreviewProvider { } .padding(.top, 20) .onAppear { - hitsController.hits = ["One", "Two", "Three"] + hitsController.hitsSource = interactor + + let results = try! JSONDecoder().decode(SearchResponse.self, from: rawHits) + + interactor.onResultsUpdated.subscribe(with: hitsController) { (reb, hit) in + reb.reload() + } + interactor.update(results) }.navigationBarTitle("Hits") } + NavigationView { HitsList(hitsController) { string, _ in VStack { HStack { - Text(string ?? "---") + Text(string?.value ?? "---") } Divider() } From 5db58532e5c76ae24285a64adcb227be29740c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Mon, 23 Jan 2023 09:04:00 +0100 Subject: [PATCH 5/7] Style: fix indentation in HitsList Preview --- Sources/InstantSearchSwiftUI/View/HitsList.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/InstantSearchSwiftUI/View/HitsList.swift b/Sources/InstantSearchSwiftUI/View/HitsList.swift index 2ca40a43..f13b4b39 100644 --- a/Sources/InstantSearchSwiftUI/View/HitsList.swift +++ b/Sources/InstantSearchSwiftUI/View/HitsList.swift @@ -127,12 +127,12 @@ struct HitsView_Previews: PreviewProvider { .onAppear { hitsController.hitsSource = interactor - let results = try! JSONDecoder().decode(SearchResponse.self, from: rawHits) - - interactor.onResultsUpdated.subscribe(with: hitsController) { (reb, hit) in - reb.reload() - } - interactor.update(results) + let results = try! JSONDecoder().decode(SearchResponse.self, from: rawHits) + + interactor.onResultsUpdated.subscribe(with: hitsController) { (reb, hit) in + reb.reload() + } + interactor.update(results) }.navigationBarTitle("Hits") } From 5a499ffe11f0e195eae258179fd930ddf46eaa58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Mon, 23 Jan 2023 09:12:32 +0100 Subject: [PATCH 6/7] Feat: clear paginator's memory when showItemsOnEmptyQuery is set to false --- Sources/InstantSearchCore/Hits/HitsInteractor.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor.swift b/Sources/InstantSearchCore/Hits/HitsInteractor.swift index e01bf700..5090c3d5 100644 --- a/Sources/InstantSearchCore/Hits/HitsInteractor.swift +++ b/Sources/InstantSearchCore/Hits/HitsInteractor.swift @@ -104,6 +104,7 @@ public class HitsInteractor: AnyHitsInteractor { guard let hitsPageMap = paginator.pageMap, !paginator.isInvalidated else { return 0 } if isLastQueryEmpty && !settings.showItemsOnEmptyQuery { + clear() return 0 } else { return hitsPageMap.count From c3a7367a252527e343b0d55ee029f9486ac71a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fontana?= Date: Thu, 26 Jan 2023 18:58:03 +0100 Subject: [PATCH 7/7] Fix: HitsList previews compiler directive --- .../InstantSearchSwiftUI/View/HitsList.swift | 136 +++++++++--------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/Sources/InstantSearchSwiftUI/View/HitsList.swift b/Sources/InstantSearchSwiftUI/View/HitsList.swift index df1fe985..577cedf6 100644 --- a/Sources/InstantSearchSwiftUI/View/HitsList.swift +++ b/Sources/InstantSearchSwiftUI/View/HitsList.swift @@ -68,83 +68,85 @@ } } - @available(iOS 13.0, tvOS 13.0, watchOS 7.0, *) - struct HitsView_Previews: PreviewProvider { - - struct PreviewRecord: Codable { - let objectID: ObjectID - let value: Value + #if os(iOS) + @available(iOS 13.0, tvOS 13.0, watchOS 7.0, *) + struct HitsView_Previews: PreviewProvider { - init(_ value: Value, objectID: ObjectID = ObjectID(rawValue: UUID().uuidString)) { - self.value = value - self.objectID = objectID + struct PreviewRecord: Codable { + let objectID: ObjectID + let value: Value + + init(_ value: Value, objectID: ObjectID = ObjectID(rawValue: UUID().uuidString)) { + self.value = value + self.objectID = objectID + } + + static func withValue(_ value: Value) -> Self { + .init(value) + } } - static func withValue(_ value: Value) -> Self { - .init(value) - } - } - - static let rawHits: Data = """ - { - "hits": [ - { - "objectID": "1", - "value": "h1" - }, - { - "objectID": "2", - "value": "h2" - } - ] - } - """.data(using: .utf8)! - - static let hitsController: HitsObservableController> = .init() - - static let interactor = HitsInteractor>(infiniteScrolling: .off, showItemsOnEmptyQuery: true) - - static var previews: some View { - NavigationView { - HitsList(hitsController) { string, _ in - VStack { - HStack { - Text(string?.value ?? "---") - .frame(maxWidth: .infinity, minHeight: 30, maxHeight: .infinity, alignment: .leading) - .padding(.horizontal, 16) + static let rawHits: Data = """ + { + "hits": [ + { + "objectID": "1", + "value": "h1" + }, + { + "objectID": "2", + "value": "h2" + } + ] + } + """.data(using: .utf8)! + + static let hitsController: HitsObservableController> = .init() + + static let interactor = HitsInteractor>(infiniteScrolling: .off, showItemsOnEmptyQuery: true) + + static var previews: some View { + NavigationView { + HitsList(hitsController) { string, _ in + VStack { + HStack { + Text(string?.value ?? "---") + .frame(maxWidth: .infinity, minHeight: 30, maxHeight: .infinity, alignment: .leading) + .padding(.horizontal, 16) + } + Divider() } - Divider() + } noResults: { + Text("No results") } - } noResults: { - Text("No results") - } - .padding(.top, 20) - .onAppear { - hitsController.hitsSource = interactor - - let results = try! JSONDecoder().decode(SearchResponse.self, from: rawHits) - - interactor.onResultsUpdated.subscribe(with: hitsController) { (reb, hit) in - reb.reload() + .padding(.top, 20) + .onAppear { + hitsController.hitsSource = interactor + + let results = try! JSONDecoder().decode(SearchResponse.self, from: rawHits) + + interactor.onResultsUpdated.subscribe(with: hitsController) { (reb, hit) in + reb.reload() + } + interactor.update(results) } - interactor.update(results) + .navigationBarTitle("Hits") } - .navigationBarTitle("Hits") - } - - NavigationView { - HitsList(hitsController) { string, _ in - VStack { - HStack { - Text(string?.value ?? "---") + + NavigationView { + HitsList(hitsController) { string, _ in + VStack { + HStack { + Text(string?.value ?? "---") + } + Divider() } - Divider() + } noResults: { + Text("No results") } - } noResults: { - Text("No results") + .navigationBarTitle("Hits") } - .navigationBarTitle("Hits") } } - } + #endif #endif