diff --git a/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift index ab6bc95d..c1f8586e 100644 --- a/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift +++ b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift @@ -46,4 +46,6 @@ public protocol AnyHitsInteractor: AnyObject { func notifyQueryChanged() func process(_ error: Swift.Error, for query: Query) + + func clear() } diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor.swift b/Sources/InstantSearchCore/Hits/HitsInteractor.swift index 7e579d1f..6dd79233 100644 --- a/Sources/InstantSearchCore/Hits/HitsInteractor.swift +++ b/Sources/InstantSearchCore/Hits/HitsInteractor.swift @@ -100,6 +100,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 @@ -150,6 +151,10 @@ public class HitsInteractor: AnyHitsInteractor { infiniteScrollingController.calculatePagesAndLoad(currentRow: rowIndex, offset: pageLoadOffset, pageMap: hitsPageMap) } + + public func clear() { + paginator.clear() + } } public extension HitsInteractor { diff --git a/Sources/InstantSearchCore/Pagination/Paginator.swift b/Sources/InstantSearchCore/Pagination/Paginator.swift index 054b8eb5..072dca00 100644 --- a/Sources/InstantSearchCore/Pagination/Paginator.swift +++ b/Sources/InstantSearchCore/Pagination/Paginator.swift @@ -35,4 +35,8 @@ class Paginator { public func invalidate() { isInvalidated = true } + + public func clear() { + pageMap = nil + } } diff --git a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift index 9cf0d500..b3c883cb 100644 --- a/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift +++ b/Sources/InstantSearchSwiftUI/DataModel/HitsObservableController.swift @@ -16,11 +16,12 @@ import InstantSearchTelemetry /// 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/Sources/InstantSearchSwiftUI/View/HitsList.swift b/Sources/InstantSearchSwiftUI/View/HitsList.swift index 5b4b6edd..577cedf6 100644 --- a/Sources/InstantSearchSwiftUI/View/HitsList.swift +++ b/Sources/InstantSearchSwiftUI/View/HitsList.swift @@ -71,13 +71,46 @@ #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) } @@ -88,20 +121,30 @@ } .padding(.top, 20) .onAppear { - hitsController.hits = ["One", "Two", "Three"] - }.navigationBarTitle("Hits") + 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() } } noResults: { Text("No results") - }.navigationBarTitle("Hits") + } + .navigationBarTitle("Hits") } } } diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift index 17ddd07d..198a5e2e 100644 --- a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift @@ -220,7 +220,35 @@ class HitsInteractorTests: XCTestCase { XCTAssertEqual(hitsInteractor.hit(atIndex: 1), Person(firstName: "Helen", lastName: "Smith")) exp.fulfill() } - + 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/MultiIndexHitsInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift index 7b8d7a9d..eb5a09ed 100644 --- a/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift +++ b/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift @@ -356,5 +356,9 @@ class MultiIndexHitsInteractorTests: XCTestCase { func loadMoreResults() { didCallLoadMoreResults() } + + func clear() { + + } } } diff --git a/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift b/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift index 41391775..3fa91b3a 100644 --- a/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift +++ b/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift @@ -86,4 +86,15 @@ class PaginatorTests: XCTestCase { XCTAssertNotNil(paginator.pageMap) 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) + } + }