@@ -59,132 +59,7 @@ struct POSSearchField: View {
5959 . textInputAutocapitalization ( . never)
6060 . focused ( $isSearchFieldFocused)
6161 . onChange ( of: searchTerm) { oldValue, newValue in
62- // Cancel any ongoing search
63- searchTask? . cancel ( )
64-
65- // Capture the debounce strategy synchronously BEFORE creating the task.
66- // Use searchDebounceStrategy for non-empty search terms (actual searches),
67- // and currentDebounceStrategy for empty terms (returning to popular products).
68- let debounceStrategy = newValue. isNotEmpty ? searchable. searchDebounceStrategy : searchable. currentDebounceStrategy
69-
70- searchTask = Task {
71- // Apply debouncing based on the strategy captured at the start
72- switch debounceStrategy {
73- case . smart( let duration, let loadingDelayThreshold) :
74- // Smart debouncing: Don't debounce first keystroke, but debounce subsequent keystrokes
75- // The loading indicator behavior depends on whether there's a threshold:
76- // - With threshold: Show loading after threshold if search hasn't completed (prevents flicker)
77- // - Without threshold: Show loading immediately (responsive feel)
78-
79- let shouldDebounceNextSearchRequest = !didFinishSearch
80-
81- // Early exit if search term is empty
82- guard newValue. isNotEmpty else {
83- didFinishSearch = true
84- return
85- }
86-
87- // Start loading indicator task if we have a threshold and this is first keystroke
88- let loadingTask : Task < Void , Never > ?
89- if !shouldDebounceNextSearchRequest {
90- // First keystroke - handle loading indicators
91- if let threshold = loadingDelayThreshold {
92- // With threshold: delay showing loading to prevent flicker for fast searches
93- loadingTask = Task { @MainActor in
94- try ? await Task . sleep ( nanoseconds: threshold)
95- if !Task. isCancelled {
96- searchable. clearSearchResults ( )
97- }
98- }
99- } else {
100- // No threshold - show loading immediately for responsive feel
101- searchable. clearSearchResults ( )
102- loadingTask = nil
103- }
104- } else {
105- // Subsequent keystrokes - loading already showing from previous search
106- loadingTask = nil
107- }
108-
109- if shouldDebounceNextSearchRequest {
110- try ? await Task . sleep ( nanoseconds: duration)
111- }
112-
113- // Now perform the search (common code for both and subsequent keystrokes)
114- guard !Task. isCancelled else {
115- loadingTask? . cancel ( )
116- return
117- }
118-
119- didFinishSearch = false
120- await searchable. performSearch ( term: newValue)
121-
122- // Cancel loading task if search completed (only relevant for first keystroke with threshold)
123- loadingTask? . cancel ( )
124-
125- if !Task. isCancelled {
126- didFinishSearch = true
127- }
128- return
129-
130- case . simple( let duration, let loadingDelayThreshold) :
131- // Simple debouncing: Always debounce
132- try ? await Task . sleep ( nanoseconds: duration)
133-
134- guard !Task. isCancelled else { return }
135- guard newValue. isNotEmpty else {
136- didFinishSearch = true
137- return
138- }
139-
140- didFinishSearch = false
141-
142- if let threshold = loadingDelayThreshold {
143- // Delay showing loading indicators to avoid flicker for fast queries
144- // Create a loading task that shows indicators after threshold
145- let loadingTask = Task { @MainActor in
146- try ? await Task . sleep ( nanoseconds: threshold)
147- // Only show loading if not cancelled
148- if !Task. isCancelled {
149- searchable. clearSearchResults ( )
150- }
151- }
152-
153- // Perform the search
154- await searchable. performSearch ( term: newValue)
155-
156- // Cancel loading task if search completed before threshold
157- loadingTask. cancel ( )
158- } else {
159- // No loading delay threshold - show loading immediately
160- searchable. clearSearchResults ( )
161- await searchable. performSearch ( term: newValue)
162- }
163-
164- if !Task. isCancelled {
165- didFinishSearch = true
166- }
167- return
168-
169- case . immediate:
170- // No debouncing
171- break
172- }
173-
174- guard !Task. isCancelled else { return }
175-
176- guard newValue. isNotEmpty else {
177- didFinishSearch = true
178- return
179- }
180-
181- didFinishSearch = false
182- await searchable. performSearch ( term: newValue)
183-
184- if !Task. isCancelled {
185- didFinishSearch = true
186- }
187- }
62+ handleSearchTermChange ( newValue)
18863 }
18964 }
19065 . onChange ( of: keyboardObserver. isKeyboardVisible) { _, isVisible in
@@ -197,6 +72,142 @@ struct POSSearchField: View {
19772 }
19873}
19974
75+ // MARK: - Search Handling
76+ private extension POSSearchField {
77+ func handleSearchTermChange( _ newValue: String ) {
78+ searchTask? . cancel ( )
79+
80+ let debounceStrategy = selectDebounceStrategy ( for: newValue)
81+
82+ searchTask = Task {
83+ await executeSearchWithStrategy ( debounceStrategy, searchTerm: newValue)
84+ }
85+ }
86+
87+ func selectDebounceStrategy( for searchTerm: String ) -> SearchDebounceStrategy {
88+ // Use searchDebounceStrategy for non-empty search terms (actual searches),
89+ // and currentDebounceStrategy for empty terms (returning to popular products).
90+ searchTerm. isNotEmpty ? searchable. searchDebounceStrategy : searchable. currentDebounceStrategy
91+ }
92+
93+ func executeSearchWithStrategy( _ strategy: SearchDebounceStrategy , searchTerm: String ) async {
94+ switch strategy {
95+ case . smart( let duration, let loadingDelayThreshold) :
96+ await executeSmartDebouncedSearch ( duration: duration, loadingDelayThreshold: loadingDelayThreshold, searchTerm: searchTerm)
97+ case . simple( let duration, let loadingDelayThreshold) :
98+ await executeSimpleDebouncedSearch ( duration: duration, loadingDelayThreshold: loadingDelayThreshold, searchTerm: searchTerm)
99+ case . immediate:
100+ await executeImmediateSearch ( searchTerm: searchTerm)
101+ }
102+ }
103+
104+ func executeSmartDebouncedSearch( duration: UInt64 , loadingDelayThreshold: UInt64 ? , searchTerm: String ) async {
105+ // Smart debouncing: Don't debounce first keystroke, but debounce subsequent keystrokes
106+ // The loading indicator behavior depends on whether there's a threshold:
107+ // - With threshold: Show loading after threshold if search hasn't completed (prevents flicker)
108+ // - Without threshold: Show loading immediately (responsive feel)
109+
110+ let isFirstKeystroke = didFinishSearch
111+
112+ guard searchTerm. isNotEmpty else {
113+ didFinishSearch = true
114+ return
115+ }
116+
117+ // Handle loading indicators for first keystroke
118+ let loadingTask = isFirstKeystroke ? startLoadingIndicatorTask ( threshold: loadingDelayThreshold) : nil
119+
120+ // Debounce subsequent keystrokes
121+ if !isFirstKeystroke {
122+ try ? await Task . sleep ( nanoseconds: duration)
123+ }
124+
125+ guard !Task. isCancelled else {
126+ loadingTask? . cancel ( )
127+ return
128+ }
129+
130+ await performSearchAndTrackCompletion ( searchTerm: searchTerm)
131+ loadingTask? . cancel ( )
132+ }
133+
134+ func executeSimpleDebouncedSearch( duration: UInt64 ,
135+ loadingDelayThreshold: UInt64 ? ,
136+ searchTerm: String ) async {
137+ // Simple debouncing: Always debounce every keystroke
138+ try ? await Task . sleep ( nanoseconds: duration)
139+
140+ guard !Task. isCancelled else { return }
141+ guard searchTerm. isNotEmpty else {
142+ didFinishSearch = true
143+ return
144+ }
145+
146+ didFinishSearch = false
147+
148+ if let threshold = loadingDelayThreshold {
149+ await performSearchWithDelayedLoading ( searchTerm: searchTerm, threshold: threshold)
150+ } else {
151+ searchable. clearSearchResults ( )
152+ await searchable. performSearch ( term: searchTerm)
153+ }
154+
155+ if !Task. isCancelled {
156+ didFinishSearch = true
157+ }
158+ }
159+
160+ func executeImmediateSearch( searchTerm: String ) async {
161+ guard !Task. isCancelled else { return }
162+ guard searchTerm. isNotEmpty else {
163+ didFinishSearch = true
164+ return
165+ }
166+
167+ await performSearchAndTrackCompletion ( searchTerm: searchTerm)
168+ }
169+
170+ func startLoadingIndicatorTask( threshold: UInt64 ? ) -> Task < Void , Never > ? {
171+ if let threshold {
172+ // With threshold: delay showing loading to prevent flicker for fast searches
173+ return Task { @MainActor in
174+ try ? await Task . sleep ( nanoseconds: threshold)
175+ if !Task. isCancelled {
176+ searchable. clearSearchResults ( )
177+ }
178+ }
179+ } else {
180+ // No threshold - show loading immediately for responsive feel
181+ searchable. clearSearchResults ( )
182+ return nil
183+ }
184+ }
185+
186+ func performSearchWithDelayedLoading( searchTerm: String , threshold: UInt64 ) async {
187+ // Create a loading task that shows indicators after threshold
188+ let loadingTask = Task { @MainActor in
189+ try ? await Task . sleep ( nanoseconds: threshold)
190+ if !Task. isCancelled {
191+ searchable. clearSearchResults ( )
192+ }
193+ }
194+
195+ await searchable. performSearch ( term: searchTerm)
196+
197+ // Cancel loading task if search completed before threshold
198+ loadingTask. cancel ( )
199+ }
200+
201+ private func performSearchAndTrackCompletion( searchTerm: String ) async {
202+ didFinishSearch = false
203+ await searchable. performSearch ( term: searchTerm)
204+
205+ if !Task. isCancelled {
206+ didFinishSearch = true
207+ }
208+ }
209+ }
210+
200211/// A reusable search content view for POS items
201212struct POSSearchContentView < Content: View > : View {
202213 @Environment ( \. posAnalytics) private var analytics
0 commit comments