Skip to content

Commit 5202ca4

Browse files
committed
Add loadingDelayThreshold to smart debounce strategy
Adds optional loading delay threshold parameter to smart debounce strategy. This enables delayed loading indicators for fast local searches to prevent flicker, while maintaining immediate loading indicators for remote searches. Changes: - Add optional loadingDelayThreshold parameter to SearchDebounceStrategy.smart case - Update POSSearchView to handle smart strategy with optional threshold: - Check for non-empty search term before showing loading (prevents loading on initial view) - Reset didFinishSearch to true when search view appears (ensures first search shows loading) - First keystroke without threshold: Show loading immediately (remote searches) - First keystroke with threshold: Delay loading until threshold or completion (local searches) - Subsequent keystrokes: Debounce request, loading already showing - Remote searches use .smart without threshold for immediate responsive feedback - Local searches use .simple with threshold to prevent flicker on fast queries - Update test expectations to match strategy configurations Fixes: - No loading indicators shown when opening search view with popular products - Loading indicators show immediately on first search keystroke for remote searches - Local searches avoid flicker by only showing loading if query takes longer than threshold
1 parent 3aa198b commit 5202ca4

File tree

6 files changed

+69
-13
lines changed

6 files changed

+69
-13
lines changed

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,63 @@ struct POSSearchField: View {
6363
searchTask = Task {
6464
// Apply debouncing based on the strategy from the fetch strategy
6565
switch searchable.debounceStrategy {
66-
case .smart(let duration):
67-
// Smart debouncing: Skip debounce on first keystroke after search completes,
68-
// then debounce subsequent keystrokes
69-
let shouldDebounce = !didFinishSearch
70-
if shouldDebounce {
71-
try? await Task.sleep(nanoseconds: duration)
66+
case .smart(let duration, let loadingDelayThreshold):
67+
// Smart debouncing: Don't debounce first keystroke, but debounce subsequent keystrokes
68+
// The loading indicator behavior depends on whether there's a threshold:
69+
// - With threshold: Show loading after threshold if search hasn't completed (prevents flicker)
70+
// - Without threshold: Show loading immediately (responsive feel)
71+
72+
let shouldDebounceNextSearchRequest = !didFinishSearch
73+
74+
// Early exit if search term is empty
75+
guard newValue.isNotEmpty else {
76+
didFinishSearch = true
77+
return
78+
}
79+
80+
// Start loading indicator task if we have a threshold and this is first keystroke
81+
let loadingTask: Task<Void, Never>?
82+
if !shouldDebounceNextSearchRequest {
83+
// First keystroke - handle loading indicators
84+
if let threshold = loadingDelayThreshold {
85+
// With threshold: delay showing loading to prevent flicker for fast searches
86+
loadingTask = Task { @MainActor in
87+
try? await Task.sleep(nanoseconds: threshold)
88+
if !Task.isCancelled {
89+
searchable.clearSearchResults()
90+
}
91+
}
92+
} else {
93+
// No threshold - show loading immediately for responsive feel
94+
searchable.clearSearchResults()
95+
loadingTask = nil
96+
}
7297
} else {
73-
searchable.clearSearchResults()
98+
// Subsequent keystrokes - loading already showing from previous search
99+
loadingTask = nil
74100
}
75101

102+
if shouldDebounceNextSearchRequest {
103+
try? await Task.sleep(nanoseconds: duration)
104+
}
105+
106+
// Now perform the search (common code for both and subsequent keystrokes)
107+
guard !Task.isCancelled else {
108+
loadingTask?.cancel()
109+
return
110+
}
111+
112+
didFinishSearch = false
113+
await searchable.performSearch(term: newValue)
114+
115+
// Cancel loading task if search completed (only relevant for first keystroke with threshold)
116+
loadingTask?.cancel()
117+
118+
if !Task.isCancelled {
119+
didFinishSearch = true
120+
}
121+
return
122+
76123
case .simple(let duration, let loadingDelayThreshold):
77124
// Simple debouncing: Always debounce
78125
try? await Task.sleep(nanoseconds: duration)
@@ -139,6 +186,7 @@ struct POSSearchField: View {
139186
}
140187
.onAppear {
141188
isSearchFieldFocused = true
189+
didFinishSearch = true // Reset state when search view appears
142190
}
143191
}
144192
}

Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategy.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ struct PointOfSaleSearchCouponFetchStrategy: PointOfSaleCouponFetchStrategy {
101101

102102
var debounceStrategy: SearchDebounceStrategy {
103103
// Use smart debouncing for remote coupon search
104+
// No loading delay threshold - show loading immediately for responsive feel
104105
.smart(duration: 500 * NSEC_PER_MSEC)
105106
}
106107

Modules/Sources/Yosemite/PointOfSale/Items/PointOfSalePurchasableItemFetchStrategy.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ public struct PointOfSaleSearchPurchasableItemFetchStrategy: PointOfSalePurchasa
8282

8383
public var debounceStrategy: SearchDebounceStrategy {
8484
// Use smart debouncing for remote search: don't debounce first keystroke to show loading immediately,
85-
// then debounce subsequent keystrokes while search is ongoing
85+
// then debounce subsequent keystrokes while search is ongoing.
86+
// No loading delay threshold - show loading immediately for responsive feel.
8687
.smart(duration: 500 * NSEC_PER_MSEC)
8788
}
8889

Modules/Sources/Yosemite/PointOfSale/Items/SearchDebounceStrategy.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import Foundation
33
/// Defines the debouncing behavior for search input
44
public enum SearchDebounceStrategy: Equatable {
55
/// Smart debouncing: Skip debounce on first keystroke after a search completes, then debounce subsequent keystrokes.
6+
/// Optionally delays showing loading indicators until a threshold is exceeded.
67
/// Optimized for slow network searches where the first keystroke should show loading immediately.
7-
/// - Parameter duration: The debounce duration in nanoseconds for subsequent keystrokes
8-
case smart(duration: UInt64)
8+
/// - Parameters:
9+
/// - duration: The debounce duration in nanoseconds for subsequent keystrokes
10+
/// - loadingDelayThreshold: Optional threshold in nanoseconds before showing loading indicators. If nil, shows loading immediately.
11+
case smart(duration: UInt64, loadingDelayThreshold: UInt64? = nil)
912

1013
/// Simple debouncing: Always debounce every keystroke by the specified duration.
1114
/// Optionally delays showing loading indicators until a threshold is exceeded.

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16586,6 +16586,7 @@
1658616586
CODE_SIGN_ENTITLEMENTS = "Resources/Woo-Alpha.entitlements";
1658716587
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "Resources/Woo-Alpha-macOS.entitlements";
1658816588
CODE_SIGN_STYLE = Manual;
16589+
CURRENT_PROJECT_VERSION = 23.8.0.0;
1658916590
ENABLE_BITCODE = NO;
1659016591
INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist";
1659116592
INFOPLIST_PREFIX_HEADER = DerivedSources/InfoPlist.h;
@@ -16594,7 +16595,7 @@
1659416595
"$(inherited)",
1659516596
"@executable_path/Frameworks",
1659616597
);
16597-
MARKETING_VERSION = 23.7;
16598+
MARKETING_VERSION = 23.8;
1659816599
OTHER_SWIFT_FLAGS = "$(inherited)";
1659916600
PRODUCT_BUNDLE_IDENTIFIER = com.automattic.alpha.woocommerce;
1660016601
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -17392,6 +17393,7 @@
1739217393
CODE_SIGN_ENTITLEMENTS = "Resources/Woo-Debug.entitlements";
1739317394
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "Resources/Woo-Debug-macOS.entitlements";
1739417395
CODE_SIGN_STYLE = Manual;
17396+
CURRENT_PROJECT_VERSION = 23.8.0.0;
1739517397
ENABLE_BITCODE = NO;
1739617398
INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist";
1739717399
INFOPLIST_PREFIX_HEADER = DerivedSources/InfoPlist.h;
@@ -17400,7 +17402,7 @@
1740017402
"$(inherited)",
1740117403
"@executable_path/Frameworks",
1740217404
);
17403-
MARKETING_VERSION = 23.7;
17405+
MARKETING_VERSION = 23.8;
1740417406
OTHER_SWIFT_FLAGS = "$(inherited)";
1740517407
PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce;
1740617408
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -17422,6 +17424,7 @@
1742217424
CODE_SIGN_ENTITLEMENTS = "Resources/Woo-Release.entitlements";
1742317425
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "Resources/Woo-Release-macOS.entitlements";
1742417426
CODE_SIGN_STYLE = Manual;
17427+
CURRENT_PROJECT_VERSION = 23.8.0.0;
1742517428
ENABLE_BITCODE = NO;
1742617429
INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist";
1742717430
INFOPLIST_PREFIX_HEADER = DerivedSources/InfoPlist.h;
@@ -17430,7 +17433,7 @@
1743017433
"$(inherited)",
1743117434
"@executable_path/Frameworks",
1743217435
);
17433-
MARKETING_VERSION = 23.7;
17436+
MARKETING_VERSION = 23.8;
1743417437
OTHER_SWIFT_FLAGS = "$(inherited)";
1743517438
PRODUCT_BUNDLE_IDENTIFIER = com.automattic.woocommerce;
1743617439
PRODUCT_NAME = "$(TARGET_NAME)";
Binary file not shown.

0 commit comments

Comments
 (0)