Skip to content

Commit 9656089

Browse files
joshhealdclaude
andcommitted
Fix loading indicators not showing on first remote search
The issue was that the debounce strategy was determined by the current fetch strategy, but the fetch strategy didn't change from "popular products" to "search" until performSearch() was called. This meant the first keystroke used the `.immediate` strategy from popular products, which set `didFinishSearch=false` but didn't call `clearSearchResults()` to show loading. Solution: Added `searchDebounceStrategy` property to POSSearchable protocol that returns the debounce strategy that will be used for searches, regardless of the current fetch mode. The onChange handler now captures this strategy synchronously before creating the Task, ensuring we use the search strategy (.smart) from the very first keystroke. Changes: - Added `searchDebounceStrategy` to POSSearchable protocol - Updated POSSearchView to use searchDebounceStrategy for non-empty searches - Implemented searchDebounceStrategy in controllers by creating temporary search strategy to query its debounce settings - Updated all mocks and preview helpers to conform to new protocol 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0a668f1 commit 9656089

File tree

8 files changed

+41
-3
lines changed

8 files changed

+41
-3
lines changed

Modules/Sources/PointOfSale/Controllers/PointOfSaleCouponsController.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ protocol PointOfSaleCouponsControllerProtocol: PointOfSaleSearchingItemsControll
6262
fetchStrategy.debounceStrategy
6363
}
6464

65+
var searchDebounceStrategy: SearchDebounceStrategy {
66+
// Return the debounce strategy that would be used for a search
67+
let searchStrategy = fetchStrategyFactory.searchStrategy(searchTerm: "",
68+
analytics: POSItemFetchAnalytics(itemType: .coupon,
69+
analytics: analyticsProvider))
70+
return searchStrategy.debounceStrategy
71+
}
72+
6573
@MainActor
6674
func loadNextItems(base: ItemListBaseItem) async {
6775
guard paginationTracker.hasNextPage else {

Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController
2929
func clearSearchItems(baseItem: ItemListBaseItem)
3030
/// The debouncing strategy from the current fetch strategy
3131
var currentDebounceStrategy: SearchDebounceStrategy { get }
32+
/// The debouncing strategy that will be used when performing a search
33+
var searchDebounceStrategy: SearchDebounceStrategy { get }
3234
}
3335

3436

@@ -85,6 +87,15 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController
8587
fetchStrategy.debounceStrategy
8688
}
8789

90+
var searchDebounceStrategy: SearchDebounceStrategy {
91+
// Return the debounce strategy that would be used for a search
92+
// We create a temporary search strategy to get its debounce settings
93+
let searchStrategy = itemFetchStrategyFactory.searchStrategy(searchTerm: "",
94+
analytics: POSItemFetchAnalytics(itemType: .product,
95+
analytics: analyticsProvider))
96+
return searchStrategy.debounceStrategy
97+
}
98+
8899
@MainActor
89100
private func loadFirstPage(base: ItemListBaseItem) async {
90101
switch base {

Modules/Sources/PointOfSale/Presentation/Item Search/POSProductSearchable.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ final class POSProductSearchable: POSSearchable {
2929
itemsController.currentDebounceStrategy
3030
}
3131

32+
var searchDebounceStrategy: SearchDebounceStrategy {
33+
itemsController.searchDebounceStrategy
34+
}
35+
3236
func performSearch(term: String) async {
3337
await itemsController.searchItems(searchTerm: term, baseItem: .root)
3438
}

Modules/Sources/PointOfSale/Presentation/Item Search/POSSearchView.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ protocol POSSearchable {
1010
var searchHistory: [String] { get }
1111
/// The debouncing strategy to use for search input
1212
var debounceStrategy: SearchDebounceStrategy { get }
13+
/// The debouncing strategy that will be used when performing a search (may differ from current strategy)
14+
var searchDebounceStrategy: SearchDebounceStrategy { get }
1315

1416
/// Called when a search should be performed
1517
/// - Parameter term: The search term to use
@@ -60,9 +62,14 @@ struct POSSearchField: View {
6062
// Cancel any ongoing search
6163
searchTask?.cancel()
6264

65+
// Capture the debounce strategy synchronously BEFORE creating the task.
66+
// Use searchDebounceStrategy for non-empty search terms (actual searches),
67+
// and debounceStrategy for empty terms (returning to popular products).
68+
let debounceStrategy = newValue.isNotEmpty ? searchable.searchDebounceStrategy : searchable.debounceStrategy
69+
6370
searchTask = Task {
64-
// Apply debouncing based on the strategy from the fetch strategy
65-
switch searchable.debounceStrategy {
71+
// Apply debouncing based on the strategy captured at the start
72+
switch debounceStrategy {
6673
case .smart(let duration, let loadingDelayThreshold):
6774
// Smart debouncing: Don't debounce first keystroke, but debounce subsequent keystrokes
6875
// The loading indicator behavior depends on whether there's a threshold:
@@ -186,7 +193,6 @@ struct POSSearchField: View {
186193
}
187194
.onAppear {
188195
isSearchFieldFocused = true
189-
didFinishSearch = true // Reset state when search view appears
190196
}
191197
}
192198
}

Modules/Sources/PointOfSale/Presentation/Orders/POSOrderListView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,11 @@ final class POSOrderSearchable: POSSearchable {
372372
.smart(duration: 500 * NSEC_PER_MSEC)
373373
}
374374

375+
var searchDebounceStrategy: SearchDebounceStrategy {
376+
// Orders use the same strategy for both modes
377+
debounceStrategy
378+
}
379+
375380
func performSearch(term: String) async {
376381
await ordersController.searchOrders(searchTerm: term)
377382
}

Modules/Sources/PointOfSale/Utils/PreviewHelpers.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ final class PointOfSalePreviewCouponsController: PointOfSaleCouponsControllerPro
107107
itemsStack: ItemsStackState(root: .loading([]),
108108
itemStates: [:]))
109109
var currentDebounceStrategy: SearchDebounceStrategy { .immediate }
110+
var searchDebounceStrategy: SearchDebounceStrategy { .smart(duration: 500 * NSEC_PER_MSEC) }
110111
func enableCoupons() async { }
111112
func loadItems(base: ItemListBaseItem) async { }
112113
func refreshItems(base: ItemListBaseItem) async { }
@@ -121,6 +122,7 @@ final class PointOfSalePreviewItemsController: PointOfSaleSearchingItemsControll
121122
itemStates: [:]))
122123

123124
var currentDebounceStrategy: SearchDebounceStrategy { .immediate }
125+
var searchDebounceStrategy: SearchDebounceStrategy { .smart(duration: 500 * NSEC_PER_MSEC) }
124126

125127
func loadItems(base: ItemListBaseItem) async {
126128
switch base {

Modules/Tests/PointOfSaleTests/Mocks/MockPointOfSaleCouponsController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final class MockPointOfSaleCouponsController: PointOfSaleCouponsControllerProtoc
99
itemsStack: .init(root: .empty, itemStates: [:]))
1010

1111
var currentDebounceStrategy: SearchDebounceStrategy = .immediate
12+
var searchDebounceStrategy: SearchDebounceStrategy = .smart(duration: 500 * NSEC_PER_MSEC)
1213

1314
func loadItems(base: ItemListBaseItem) async {
1415
loadItemsCalled = true

Modules/Tests/PointOfSaleTests/Mocks/MockPointOfSalePurchasableItemsSearchController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final class MockPointOfSalePurchasableItemsSearchController: PointOfSaleSearchin
99
itemsStack: .init(root: .empty, itemStates: [:]))
1010

1111
var currentDebounceStrategy: SearchDebounceStrategy = .immediate
12+
var searchDebounceStrategy: SearchDebounceStrategy = .smart(duration: 500 * NSEC_PER_MSEC)
1213

1314
func searchItems(searchTerm: String, baseItem: ItemListBaseItem) async {}
1415

0 commit comments

Comments
 (0)