Skip to content

Commit a338c78

Browse files
committed
feat: TodoViewModel 개선 및 삭제 취소에 대한 토스트 추가
1 parent 794f420 commit a338c78

2 files changed

Lines changed: 84 additions & 33 deletions

File tree

DevLog/Presentation/ViewModel/TodoViewModel.swift

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,49 @@ final class TodoViewModel: Store {
1818
var scope: TodoScope = .title
1919
var filterOption: FilterOption = .create
2020
var isLoading = false
21+
var showToast: Bool = false
22+
var toastMessage: String = ""
23+
var pendingTask: (Todo, Int)?
2124
}
2225

2326
enum FilterOption {
2427
case create, update, day, week, month, year
2528
}
2629

2730
enum Action {
28-
// Modifier
29-
case onAppear, refresh
30-
3131
// User
3232
case tapTogglePinned(Todo)
3333
case swipeTodo(Todo)
3434
case tapFilterOption(FilterOption)
3535
case upsertTodo(Todo)
36+
case undoDelete
37+
case confirmDelete
3638

37-
// Binding
39+
// View
40+
case onAppear, refresh
3841
case openEditor
3942
case closeEditor
4043
case closeAlert
4144
case setScope(TodoScope)
4245
case setSearchText(String)
46+
case setToast(isPresented: Bool, type: ToastType? = nil)
47+
case setLoading(Bool)
48+
case setTodos([Todo])
4349

44-
// Call from run
45-
case didFetchTodos([Todo])
46-
case didLoading(Bool)
50+
// Run
4751
case didShowAlert(String)
4852
case didTogglePinned(Todo)
4953
}
54+
55+
enum ToastType {
56+
case delete
57+
}
5058

5159
enum SideEffect {
52-
case fetchTodos
53-
case upsertTodo(Todo)
60+
case fetch
61+
case upsert(Todo)
62+
case delete(Todo)
5463
case togglePinned(Todo)
55-
case swipeTodo(Todo)
5664
}
5765

5866
private let fetchTodosByKindUseCase: FetchTodosByKindUseCase
@@ -73,17 +81,33 @@ final class TodoViewModel: Store {
7381
}
7482

7583
func reduce(with action: Action) -> [SideEffect] {
84+
var state = self.state
85+
7686
switch action {
7787
case .onAppear, .refresh:
78-
return [.fetchTodos]
88+
return [.fetch]
7989
case .tapTogglePinned(let todo):
8090
return [.togglePinned(todo)]
8191
case .swipeTodo(let todo):
82-
return [.swipeTodo(todo)]
92+
guard let index = state.todos.firstIndex(where: { $0.id == todo.id }) else {
93+
return []
94+
}
95+
state.pendingTask = (todo, index)
96+
state.todos.remove(at: index)
97+
setToast(&state, isPresented: true, for: .delete)
8398
case .tapFilterOption(let option):
8499
state.filterOption = option
85100
case .upsertTodo(let todo):
86-
return [.upsertTodo(todo)]
101+
return [.upsert(todo)]
102+
case .undoDelete:
103+
guard let (todo, index) = state.pendingTask else { return [] }
104+
state.todos.insert(todo, at: index)
105+
state.pendingTask = nil
106+
case .confirmDelete:
107+
guard let (item, _) = state.pendingTask else {
108+
return []
109+
}
110+
return [.delete(item)]
87111
case .openEditor:
88112
state.showEditor = true
89113
case .closeEditor:
@@ -94,10 +118,12 @@ final class TodoViewModel: Store {
94118
state.scope = scope
95119
case .setSearchText(let text):
96120
state.searchText = text
97-
case .didFetchTodos(let todos):
98-
state.todos = todos
99-
case .didLoading(let value):
121+
case .setToast(let isPresented, let type):
122+
setToast(&state, isPresented: isPresented, for: type)
123+
case .setLoading(let value):
100124
state.isLoading = value
125+
case .setTodos(let todos):
126+
state.todos = todos
101127
case .didShowAlert(let message):
102128
state.alertMessage = message
103129
state.showAlert = true
@@ -106,57 +132,71 @@ final class TodoViewModel: Store {
106132
state.todos[index] = todo
107133
}
108134
}
135+
136+
self.state = state
109137
return []
110138
}
111139

112140
func run(_ effect: SideEffect) {
113141
switch effect {
114-
case .fetchTodos:
142+
case .fetch:
115143
Task {
116144
do {
117-
defer { send(.didLoading(false)) }
118-
send(.didLoading(true))
145+
defer { send(.setLoading(false)) }
146+
send(.setLoading(true))
119147
let todos = try await fetchTodosByKindUseCase.execute(state.kind)
120-
send(.didFetchTodos(todos))
148+
send(.setTodos(todos))
121149
} catch {
122150
send(.didShowAlert(error.localizedDescription))
123151
}
124152
}
125-
case .upsertTodo(let todo):
153+
case .upsert(let item):
126154
Task {
127155
do {
128-
defer { send(.didLoading(false)) }
129-
send(.didLoading(true))
130-
try await upsertTodoUseCase.execute(todo)
156+
defer { send(.setLoading(false)) }
157+
send(.setLoading(true))
158+
try await upsertTodoUseCase.execute(item)
131159
send(.refresh)
132160
} catch {
133161
send(.didShowAlert(error.localizedDescription))
134162
}
135163
}
136-
case .togglePinned(let todo):
164+
case .togglePinned(let item):
137165
Task {
138166
do {
139-
defer { send(.didLoading(false)) }
140-
send(.didLoading(true))
141-
var todo = todo
167+
defer { send(.setLoading(false)) }
168+
send(.setLoading(true))
169+
var todo = item
142170
todo.isPinned.toggle()
143171
try await upsertTodoUseCase.execute(todo)
144172
send(.didTogglePinned(todo))
145173
} catch {
146174
send(.didShowAlert(error.localizedDescription))
147175
}
148176
}
149-
case .swipeTodo(let todo):
177+
case .delete(let item):
150178
Task {
151179
do {
152-
defer { send(.didLoading(false)) }
153-
send(.didLoading(true))
154-
try await deleteTodoUseCase.execute(todo.id)
155-
send(.refresh)
180+
try await deleteTodoUseCase.execute(item.id)
156181
} catch {
157182
send(.didShowAlert(error.localizedDescription))
158183
}
159184
}
160185
}
161186
}
162187
}
188+
private extension TodoViewModel {
189+
func setToast(
190+
_ state: inout State,
191+
isPresented: Bool,
192+
for type: ToastType?
193+
) {
194+
switch type {
195+
case .delete:
196+
state.toastMessage = "실행 취소"
197+
case .none:
198+
state.toastMessage = ""
199+
}
200+
state.showToast = isPresented
201+
}
202+
}

DevLog/UI/Home/TodoView.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ struct TodoView: View {
9292
} message: {
9393
Text(viewModel.state.alertMessage)
9494
}
95+
.toast(
96+
isPresented: Binding(
97+
get: { viewModel.state.showToast },
98+
set: { viewModel.send(.setToast(isPresented: $0)) }
99+
),
100+
duration: 5,
101+
action: { viewModel.send(.undoDelete) },
102+
onDismiss: { viewModel.send(.confirmDelete) }
103+
) {
104+
Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left")
105+
}
95106
.navigationTitle(viewModel.state.kind.localizedName)
96107
.navigationBarTitleDisplayMode(.large)
97108
.fullScreenCover(isPresented: Binding(

0 commit comments

Comments
 (0)