@@ -11,12 +11,14 @@ final class TodoListViewModel: Store {
1111 struct State {
1212 var todos : [ TodoListItem ] = [ ]
1313 var searchText : String = " "
14+ var searchResults : [ TodoListItem ] = [ ]
1415 let kind : TodoKind
1516 var showEditor : Bool = false
1617 var showAlert : Bool = false
1718 var alertTitle : String = " "
1819 var alertMessage : String = " "
19- var scope : TodoScope = . title
20+ var isSearching : Bool = false
21+ var showAllSearchResults : Bool = false
2022 var query : TodoQuery
2123 var isLoading : Bool = false
2224 var showToast : Bool = false
@@ -37,6 +39,8 @@ final class TodoListViewModel: Store {
3739 case togglePinnedOnly
3840 case setCompletionFilter( TodoQuery . CompletionFilter )
3941 case resetFilters
42+ case setIsSearching( Bool )
43+ case setShowAllSearchResults( Bool )
4044 case tapToggleCompleted( TodoListItem )
4145 case tapTogglePinned( TodoListItem )
4246 case undoDelete
@@ -45,12 +49,13 @@ final class TodoListViewModel: Store {
4549 case confirmDelete
4650 case onAppear
4751 case loadNextPage
48- case setScope( TodoScope )
4952 case setSearchText( String )
5053 case setToast( isPresented: Bool )
5154 case upsertTodo( Todo )
5255
5356 // Run
57+ case setSearchQuery( String )
58+ case fetchSearchResults( [ TodoListItem ] )
5459 case didToggleCompleted( TodoListItem )
5560 case didTogglePinned( TodoListItem )
5661 case setLoading( Bool )
@@ -62,13 +67,16 @@ final class TodoListViewModel: Store {
6267 enum SideEffect {
6368 case fetch
6469 case loadNextPage
70+ case search( String )
6571 case upsert( Todo )
6672 case delete( String )
6773 case toggleCompleted( TodoListItem )
6874 case togglePinned( TodoListItem )
6975 }
7076
7177 @Published private( set) var state : State
78+ private let searchDebounceDelay : Double = 0.4
79+ private var searchDebounceTask : Task < Void , Never > ?
7280 private let fetchTodosUseCase : FetchTodosUseCase
7381 private let fetchTodoByIDUseCase : FetchTodoByIDUseCase
7482 private let upsertTodoUseCase : UpsertTodoUseCase
@@ -91,6 +99,8 @@ final class TodoListViewModel: Store {
9199 )
92100 }
93101
102+ let searchResultsLimit = 5
103+
94104 var appliedFilterCount : Int {
95105 var count = 0
96106 if state. query. sortTarget != . createdAt { count += 1 }
@@ -106,14 +116,15 @@ final class TodoListViewModel: Store {
106116
107117 switch action {
108118 case . refresh, . setAlert, . setShowEditor, . swipeTodo, . setSortTarget, . setSortOrder,
109- . togglePinnedOnly, . setCompletionFilter, . resetFilters, . tapToggleCompleted ,
110- . tapTogglePinned, . undoDelete:
119+ . togglePinnedOnly, . setCompletionFilter, . resetFilters, . setIsSearching ,
120+ . setShowAllSearchResults , . tapToggleCompleted , . tapTogglePinned, . undoDelete:
111121 effects = reduceByUser ( action, state: & state)
112122
113- case . confirmDelete, . onAppear, . loadNextPage, . setScope , . setSearchText, . setToast, . upsertTodo:
123+ case . confirmDelete, . onAppear, . loadNextPage, . setSearchText, . setToast, . upsertTodo:
114124 effects = reduceByView ( action, state: & state)
115125
116- case . didToggleCompleted, . didTogglePinned, . setLoading, . appendTodos, . resetPagination, . setHasMore:
126+ case . setSearchQuery, . fetchSearchResults,
127+ . didToggleCompleted, . didTogglePinned, . setLoading, . appendTodos, . resetPagination, . setHasMore:
117128 effects = reduceByRun ( action, state: & state)
118129 }
119130
@@ -150,6 +161,18 @@ final class TodoListViewModel: Store {
150161 send ( . setAlert( true ) )
151162 }
152163 }
164+ case . search( let keyword) :
165+ Task {
166+ do {
167+ defer { send ( . setLoading( false ) ) }
168+ send ( . setLoading( true ) )
169+ let query = TodoQuery ( kind: state. kind, keyword: keyword)
170+ let page = try await fetchTodosUseCase. execute ( query, cursor: nil )
171+ send ( . fetchSearchResults( page. items. map { TodoListItem ( from: $0) } ) )
172+ } catch {
173+ send ( . setAlert( true ) )
174+ }
175+ }
153176 case . upsert( let item) :
154177 Task {
155178 do {
@@ -246,6 +269,16 @@ private extension TodoListViewModel {
246269 state. query = TodoQuery ( kind: state. kind)
247270 state. nextCursor = nil
248271 return [ . fetch]
272+ case . setIsSearching( let value) :
273+ state. isSearching = value
274+ if !value {
275+ cancelDebounce ( )
276+ state. searchText = " "
277+ state. searchResults = [ ]
278+ state. showAllSearchResults = false
279+ }
280+ case . setShowAllSearchResults( let value) :
281+ state. showAllSearchResults = value
249282 case . tapToggleCompleted( let todo) :
250283 return [ . toggleCompleted( todo) ]
251284 case . tapTogglePinned( let todo) :
@@ -275,10 +308,18 @@ private extension TodoListViewModel {
275308 case . loadNextPage:
276309 guard state. hasMore, !state. isLoading, state. pendingTask == nil else { return [ ] }
277310 return [ . loadNextPage]
278- case . setScope( let scope) :
279- state. scope = scope
280311 case . setSearchText( let text) :
281312 state. searchText = text
313+ state. showAllSearchResults = false
314+ let trimmed = text. trimmingCharacters ( in: . whitespacesAndNewlines)
315+ if trimmed. isEmpty {
316+ cancelDebounce ( )
317+ state. searchResults = [ ]
318+ state. isLoading = false
319+ } else {
320+ state. isLoading = true
321+ scheduleDebouncedQuery ( text)
322+ }
282323 case . setToast( let isPresented) :
283324 setToast ( & state, isPresented: isPresented)
284325 case . upsertTodo( let todo) :
@@ -291,6 +332,15 @@ private extension TodoListViewModel {
291332
292333 func reduceByRun( _ action: Action , state: inout State ) -> [ SideEffect ] {
293334 switch action {
335+ case . setSearchQuery( let query) :
336+ let trimmed = query. trimmingCharacters ( in: . whitespacesAndNewlines)
337+ if trimmed. isEmpty {
338+ state. searchResults = [ ]
339+ } else {
340+ return [ . search( trimmed) ]
341+ }
342+ case . fetchSearchResults( let items) :
343+ state. searchResults = items
294344 case . didToggleCompleted( let todo) :
295345 if let index = state. todos. firstIndex ( where: { $0. id == todo. id } ) {
296346 state. todos [ index] = todo
@@ -341,6 +391,23 @@ private extension TodoListViewModel {
341391 state. toastMessage = " 실행 취소 "
342392 state. showToast = isPresented
343393 }
394+
395+ func scheduleDebouncedQuery( _ query: String ) {
396+ searchDebounceTask? . cancel ( )
397+ searchDebounceTask = Task { [ weak self] in
398+ guard let self else { return }
399+ try ? await Task . sleep ( for: . seconds( searchDebounceDelay) )
400+ if Task . isCancelled { return }
401+ await MainActor . run {
402+ self . send ( . setSearchQuery( query) )
403+ }
404+ }
405+ }
406+
407+ func cancelDebounce( ) {
408+ searchDebounceTask? . cancel ( )
409+ searchDebounceTask = nil
410+ }
344411}
345412
346413extension TodoQuery . SortTarget {
0 commit comments