@@ -16,7 +16,7 @@ struct SearchFeature {
1616 @ObservableState
1717 struct State : Equatable {
1818 @Presents var alert : AlertState < Never > ?
19- var isLoading = false
19+ var loading = LoadingFeature . State ( )
2020 var isSearching = false
2121 var searchQuery = " "
2222 var webPages : [ WebPageItem ] = [ ]
@@ -30,6 +30,10 @@ struct SearchFeature {
3030 self . recentQueries = OrderedSet ( recentQueries)
3131 }
3232
33+ var isLoading : Bool {
34+ loading. isLoading
35+ }
36+
3337 var visibleTodos : [ TodoListItem ] {
3438 if showAllTodos {
3539 return todos
@@ -65,9 +69,9 @@ struct SearchFeature {
6569 case clearRecentQueries
6670 case applySearchQuery( String )
6771 case setAlert( Bool )
68- case setLoading( Bool )
6972 case setShowAllTodos( Bool )
7073 case setShowAllWebPages( Bool )
74+ case loading( LoadingFeature . Action )
7175 }
7276
7377 private enum CancelID : Hashable {
@@ -84,14 +88,17 @@ struct SearchFeature {
8488 private let searchDebounceDelay = Duration . seconds ( 0.4 )
8589
8690 var body : some ReducerOf < Self > {
91+ Scope ( state: \. loading, action: \. loading) {
92+ LoadingFeature ( )
93+ }
8794 BindingReducer ( )
8895 Reduce { state, action in
8996 switch action {
9097 case . alert:
9198 break
9299 case . binding( \. isSearching) :
93100 if !state. isSearching {
94- return cancelSearchEffect ( )
101+ return cancelSearchEffect ( isLoading : state . isLoading )
95102 }
96103 case . binding( \. searchQuery) :
97104 state. showAllTodos = false
@@ -100,10 +107,10 @@ struct SearchFeature {
100107 if trimmed. isEmpty {
101108 state. webPages = [ ]
102109 state. todos = [ ]
103- return cancelSearchEffect ( )
110+ return cancelSearchEffect ( isLoading : state . isLoading )
104111 } else {
105112 return . concatenate(
106- cancelSearchEffect ( ) ,
113+ cancelSearchEffect ( isLoading : state . isLoading ) ,
107114 debounceFetchEffect ( trimmed)
108115 )
109116 }
@@ -133,18 +140,18 @@ struct SearchFeature {
133140 if trimmed. isEmpty {
134141 state. webPages = [ ]
135142 state. todos = [ ]
136- return cancelSearchEffect ( )
143+ return cancelSearchEffect ( isLoading : state . isLoading )
137144 } else {
138- return fetchEffect ( trimmed)
145+ return fetchEffect ( trimmed, isLoading : state . isLoading )
139146 }
140147 case . setAlert( let isPresented) :
141148 state. alert = isPresented ? alertState ( ) : nil
142- case . setLoading( let isLoading) :
143- state. isLoading = isLoading
144149 case . setShowAllTodos( let shouldShowAll) :
145150 state. showAllTodos = shouldShowAll
146151 case . setShowAllWebPages( let shouldShowAll) :
147152 state. showAllWebPages = shouldShowAll
153+ case . loading:
154+ break
148155 }
149156
150157 return . none
@@ -201,17 +208,17 @@ private enum SearchUpdateRecentQueriesUseCaseKey: DependencyKey {
201208}
202209
203210private extension SearchFeature {
204- func cancelSearchEffect( ) -> Effect < Action > {
211+ func cancelSearchEffect( isLoading : Bool ) -> Effect < Action > {
205212 . merge(
206213 . cancel( id: CancelID . debounce) ,
207214 . cancel( id: CancelID . request) ,
208- . send ( . setLoading ( false ) )
215+ endLoadingEffect ( isLoading : isLoading )
209216 )
210217 }
211218
212219 func debounceFetchEffect( _ query: String ) -> Effect < Action > {
213220 . concatenate(
214- . send( . setLoading ( true ) ) ,
221+ . send( . loading ( . begin ( target : . default , mode : . immediate ) ) ) ,
215222 . run { [ clock, searchDebounceDelay] send in
216223 try await clock. sleep ( for: searchDebounceDelay)
217224 await send ( . applySearchQuery( query) )
@@ -220,7 +227,7 @@ private extension SearchFeature {
220227 )
221228 }
222229
223- func fetchEffect( _ query: String ) -> Effect < Action > {
230+ func fetchEffect( _ query: String , isLoading : Bool ) -> Effect < Action > {
224231 let searchesTodoOnly = searchesTodoOnly ( query)
225232
226233 return . run { [ fetchTodosUseCase, fetchWebPagesUseCase] send in
@@ -235,17 +242,26 @@ private extension SearchFeature {
235242 let resolvedWebPageItems = try await webPageItems
236243 await send ( . fetchTodos( todoItems) )
237244 await send ( . fetchWebPage( resolvedWebPageItems) )
238- await send ( . setLoading( false ) )
245+ if isLoading {
246+ await send ( . loading( . end( target: . default, mode: . immediate) ) )
247+ }
239248 } catch is CancellationError {
240249 return
241250 } catch {
242- await send ( . setLoading( false ) )
251+ if isLoading {
252+ await send ( . loading( . end( target: . default, mode: . immediate) ) )
253+ }
243254 await send ( . setAlert( true ) )
244255 }
245256 }
246257 . cancellable ( id: CancelID . request, cancelInFlight: true )
247258 }
248259
260+ func endLoadingEffect( isLoading: Bool ) -> Effect < Action > {
261+ guard isLoading else { return . none }
262+ return . send( . loading( . end( target: . default, mode: . immediate) ) )
263+ }
264+
249265 func saveRecentQueriesEffect( _ queries: OrderedSet < String > ) -> Effect < Action > {
250266 let values = Array ( queries)
251267 return . run { [ updateRecentSearchQueriesUseCase] _ in
0 commit comments