@@ -43,16 +43,22 @@ final class SearchViewModel: Store {
4343 case fetch( String )
4444 }
4545
46+ private enum SearchTaskKind : Hashable {
47+ case debounce
48+ case request
49+ }
50+
4651 private( set) var state : State = . init( )
4752 private let fetchWebPagesUseCase : FetchWebPagesUseCase
4853 private let fetchTodosUseCase : FetchTodosUseCase
4954 private let fetchRecentSearchQueriesUseCase : FetchRecentSearchQueriesUseCase
5055 private let updateRecentSearchQueriesUseCase : UpdateRecentSearchQueriesUseCase
56+ private let loadingState = LoadingState ( )
5157 let contentsLimit : Int = 5
5258
5359 private let maxRecentQueries = 20
5460 private let searchDebounceDelay : Double = 0.4
55- private var searchDebounceTask : Task < Void , Never > ?
61+ private var searchTasks : [ SearchTaskKind : Task < Void , Never > ] = [ : ]
5662
5763 init (
5864 fetchWebPagesUseCase: FetchWebPagesUseCase ,
@@ -104,22 +110,16 @@ final class SearchViewModel: Store {
104110 state. showAllWebPages = false
105111 let trimmed = query. trimmingCharacters ( in: . whitespacesAndNewlines)
106112 if trimmed. isEmpty {
107- cancelDebounce ( )
113+ cancelSearch ( )
108114 state. webPages = [ ]
109115 state. todos = [ ]
110- state. isLoading = false
111116 } else {
112- state. isLoading = true
113- scheduleDebouncedQuery ( query)
117+ cancelSearch ( )
118+ beginLoading ( . immediate)
119+ scheduleDebouncedFetch ( trimmed)
114120 }
115121 case . applySearchQuery( let query) :
116- let trimmed = query. trimmingCharacters ( in: . whitespacesAndNewlines)
117- if trimmed. isEmpty {
118- state. webPages = [ ]
119- state. todos = [ ]
120- } else {
121- effects = [ . fetch( trimmed) ]
122- }
122+ effects = [ . fetch( query) ]
123123 case . setShowAllTodos( let shouldShowAll) :
124124 state. showAllTodos = shouldShowAll
125125 case . setShowAllWebPages( let shouldShowAll) :
@@ -133,10 +133,16 @@ final class SearchViewModel: Store {
133133 func run( _ effect: SideEffect ) {
134134 switch effect {
135135 case . fetch( let query) :
136- Task {
136+ searchTasks [ . request] ? . cancel ( )
137+ let requestTask = Task { [ weak self] in
138+ guard let self else { return }
137139 do {
138- send ( . setLoading( true ) )
139- defer { send ( . setLoading( false ) ) }
140+ defer {
141+ self . searchTasks [ . request] = nil
142+ if !Task. isCancelled {
143+ self . endLoading ( . immediate)
144+ }
145+ }
140146 let searchesTodoOnly = searchesTodoOnly ( query)
141147 async let todos = fetchTodosUseCase. execute ( TodoQuery ( keyword: query) , cursor: nil )
142148 async let webPageItems = fetchWebPageItems (
@@ -145,12 +151,15 @@ final class SearchViewModel: Store {
145151 )
146152 let todoItems = try await todos. items. compactMap { TodoListItem ( from: $0) }
147153 let resolvedWebPageItems = try await webPageItems
154+ if Task . isCancelled { return }
148155 send ( . fetchTodos( todoItems) )
149156 send ( . fetchWebPage( resolvedWebPageItems) )
150157 } catch {
158+ if error is CancellationError { return }
151159 send ( . setAlert( true ) )
152160 }
153161 }
162+ searchTasks [ . request] = requestTask
154163 }
155164 }
156165}
@@ -165,21 +174,36 @@ private extension SearchViewModel {
165174 state. showAlert = isPresented
166175 }
167176
168- func scheduleDebouncedQuery ( _ query: String ) {
169- searchDebounceTask ? . cancel ( )
170- searchDebounceTask = Task { [ weak self] in
177+ func scheduleDebouncedFetch ( _ query: String ) {
178+ searchTasks [ . debounce ] ? . cancel ( )
179+ let debounceTask = Task { [ weak self] in
171180 guard let self else { return }
172181 try ? await Task . sleep ( for: . seconds( searchDebounceDelay) )
173182 if Task . isCancelled { return }
174183 await MainActor . run {
184+ self . searchTasks [ . debounce] = nil
175185 self . send ( . applySearchQuery( query) )
176186 }
177187 }
188+ searchTasks [ . debounce] = debounceTask
189+ }
190+
191+ func cancelSearch( ) {
192+ searchTasks. values. forEach { $0. cancel ( ) }
193+ searchTasks = [ : ]
194+ endLoading ( . immediate)
178195 }
179196
180- func cancelDebounce( ) {
181- searchDebounceTask? . cancel ( )
182- searchDebounceTask = nil
197+ func beginLoading( _ mode: LoadingState . Mode ) {
198+ loadingState. begin ( mode: mode) { [ weak self] isLoading in
199+ self ? . send ( . setLoading( isLoading) )
200+ }
201+ }
202+
203+ func endLoading( _ mode: LoadingState . Mode ) {
204+ loadingState. end ( mode: mode) { [ weak self] isLoading in
205+ self ? . send ( . setLoading( isLoading) )
206+ }
183207 }
184208
185209 func searchesTodoOnly( _ query: String ) -> Bool {
0 commit comments