Skip to content

Commit f6b42a1

Browse files
authored
[#35, 39] TodoViewModel를 개선한다 (#74)
* feat: 도메인 레이어 구현 및 등록 * feat: TodoViewModel 개선 및 삭제 취소에 대한 토스트 추가 * refactor: TodoViewModel의 린트 경고 개선 및 Action 최적화, 얼럿 고정
1 parent 009a826 commit f6b42a1

8 files changed

Lines changed: 206 additions & 81 deletions

File tree

DevLog/App/Assembler/DomainAssembler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ final class DomainAssembler: Assembler {
2626
container.register(UpsertTodoUseCase.self) {
2727
UpsertTodoUseCaseImpl(container.resolve(TodoRepository.self))
2828
}
29+
30+
container.register(DeleteTodoUseCase.self) {
31+
DeleteTodoUseCaseImpl(container.resolve(TodoRepository.self))
32+
}
2933

3034
container.register(AuthSessionUseCase.self) {
3135
AuthSessionUseCaseImpl(container.resolve(AuthSessionRepository.self))
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// DeleteTodoUseCase.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/12/26.
6+
//
7+
8+
protocol DeleteTodoUseCase {
9+
func execute(_ todoID: String) async throws
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// DeleteTodoUseCaseImpl.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/12/26.
6+
//
7+
8+
final class DeleteTodoUseCaseImpl: DeleteTodoUseCase {
9+
private let repository: TodoRepository
10+
11+
init(_ repository: TodoRepository) {
12+
self.repository = repository
13+
}
14+
15+
func execute(_ todoID: String) async throws {
16+
try await repository.deleteTodo(todoID)
17+
}
18+
}

DevLog/Presentation/ViewModel/TodoViewModel.swift

Lines changed: 150 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,137 +14,223 @@ final class TodoViewModel: Store {
1414
let kind: TodoKind
1515
var showEditor: Bool = false
1616
var showAlert: Bool = false
17+
var alertTitle: String = ""
1718
var alertMessage: String = ""
1819
var scope: TodoScope = .title
1920
var filterOption: FilterOption = .create
20-
var isLoading = false
21+
var isLoading: Bool = false
22+
var showToast: Bool = false
23+
var toastMessage: String = ""
24+
var pendingTask: (Todo, Int)?
2125
}
2226

2327
enum FilterOption {
2428
case create, update, day, week, month, year
2529
}
2630

2731
enum Action {
28-
// Modifier
29-
case onAppear, refresh
30-
3132
// User
32-
case tapTogglePinned(Todo)
33+
case refresh
34+
case setAlert(Bool)
35+
case setShowEditor(Bool)
3336
case swipeTodo(Todo)
3437
case tapFilterOption(FilterOption)
35-
case upsertTodo(Todo)
38+
case tapTogglePinned(Todo)
39+
case undoDelete
3640

37-
// Binding
38-
case openEditor
39-
case closeEditor
40-
case closeAlert
41+
// View
42+
case confirmDelete
43+
case onAppear
4144
case setScope(TodoScope)
4245
case setSearchText(String)
46+
case setToast(isPresented: Bool)
47+
case upsertTodo(Todo)
4348

44-
// Call from run
45-
case didFetchTodos([Todo])
46-
case didLoading(Bool)
47-
case didShowAlert(String)
49+
// Run
4850
case didTogglePinned(Todo)
51+
case setLoading(Bool)
52+
case setTodos([Todo])
4953
}
5054

5155
enum SideEffect {
52-
case fetchTodos
53-
case upsertTodo(Todo)
56+
case fetch
57+
case upsert(Todo)
58+
case delete(Todo)
5459
case togglePinned(Todo)
55-
case swipeTodo(Todo)
5660
}
5761

5862
private let fetchTodosByKindUseCase: FetchTodosByKindUseCase
5963
private let upsertTodoUseCase: UpsertTodoUseCase
64+
private let deleteTodoUseCase: DeleteTodoUseCase
6065
@Published private(set) var state: State
6166

6267
init(
6368
fetchTodosByKindUseCase: FetchTodosByKindUseCase,
6469
upsertTodoUseCase: UpsertTodoUseCase,
70+
deleteTodoUseCase: DeleteTodoUseCase,
6571
kind: TodoKind
6672
) {
6773
self.fetchTodosByKindUseCase = fetchTodosByKindUseCase
6874
self.upsertTodoUseCase = upsertTodoUseCase
75+
self.deleteTodoUseCase = deleteTodoUseCase
6976
self.state = State(kind: kind)
7077
}
7178

7279
func reduce(with action: Action) -> [SideEffect] {
80+
var state = self.state
81+
var effects: [SideEffect] = []
82+
7383
switch action {
74-
case .onAppear, .refresh:
75-
return [.fetchTodos]
76-
case .tapTogglePinned(let todo):
77-
return [.togglePinned(todo)]
78-
case .swipeTodo(let todo):
79-
return [.swipeTodo(todo)]
80-
case .tapFilterOption(let option):
81-
state.filterOption = option
82-
case .upsertTodo(let todo):
83-
return [.upsertTodo(todo)]
84-
case .openEditor:
85-
state.showEditor = true
86-
case .closeEditor:
87-
state.showEditor = false
88-
case .closeAlert:
89-
state.showAlert = false
90-
case .setScope(let scope):
91-
state.scope = scope
92-
case .setSearchText(let text):
93-
state.searchText = text
94-
case .didFetchTodos(let todos):
95-
state.todos = todos
96-
case .didLoading(let value):
97-
state.isLoading = value
98-
case .didShowAlert(let message):
99-
state.alertMessage = message
100-
state.showAlert = true
101-
case .didTogglePinned(let todo):
102-
if let index = state.todos.firstIndex(where: { $0.id == todo.id }) {
103-
state.todos[index] = todo
104-
}
84+
case .refresh, .setAlert, .setShowEditor, .swipeTodo, .tapFilterOption, .tapTogglePinned, .undoDelete:
85+
effects = reduceByUser(action, state: &state)
86+
87+
case .confirmDelete, .onAppear, .setScope, .setSearchText, .setToast, .upsertTodo:
88+
effects = reduceByView(action, state: &state)
89+
90+
case .didTogglePinned, .setLoading, .setTodos:
91+
effects = reduceByRun(action, state: &state)
10592
}
106-
return []
93+
94+
self.state = state
95+
return effects
10796
}
10897

10998
func run(_ effect: SideEffect) {
11099
switch effect {
111-
case .fetchTodos:
100+
case .fetch:
112101
Task {
113102
do {
114-
defer { send(.didLoading(false)) }
115-
send(.didLoading(true))
103+
defer { send(.setLoading(false)) }
104+
send(.setLoading(true))
116105
let todos = try await fetchTodosByKindUseCase.execute(state.kind)
117-
send(.didFetchTodos(todos))
106+
send(.setTodos(todos))
118107
} catch {
119-
send(.didShowAlert(error.localizedDescription))
108+
send(.setAlert(true))
120109
}
121110
}
122-
case .upsertTodo(let todo):
111+
case .upsert(let item):
123112
Task {
124113
do {
125-
defer { send(.didLoading(false)) }
126-
send(.didLoading(true))
127-
try await upsertTodoUseCase.execute(todo)
114+
defer { send(.setLoading(false)) }
115+
send(.setLoading(true))
116+
try await upsertTodoUseCase.execute(item)
128117
send(.refresh)
129118
} catch {
130-
send(.didShowAlert(error.localizedDescription))
119+
send(.setAlert(true))
131120
}
132121
}
133-
case .togglePinned(let todo):
122+
case .togglePinned(let item):
134123
Task {
135124
do {
136-
defer { send(.didLoading(false)) }
137-
send(.didLoading(true))
138-
var todo = todo
125+
defer { send(.setLoading(false)) }
126+
send(.setLoading(true))
127+
var todo = item
139128
todo.isPinned.toggle()
140129
try await upsertTodoUseCase.execute(todo)
141130
send(.didTogglePinned(todo))
142131
} catch {
143-
send(.didShowAlert(error.localizedDescription))
132+
send(.setAlert(true))
144133
}
145134
}
135+
case .delete(let item):
136+
Task {
137+
do {
138+
try await deleteTodoUseCase.execute(item.id)
139+
} catch {
140+
send(.setAlert(true))
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
// MARK: - Reduce Methods
148+
private extension TodoViewModel {
149+
func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] {
150+
switch action {
151+
case .refresh:
152+
return [.fetch]
153+
case .setAlert(let value):
154+
setAlert(&state, isPresented: value)
155+
case .setShowEditor(let value):
156+
state.showEditor = value
146157
case .swipeTodo(let todo):
158+
guard let index = state.todos.firstIndex(where: { $0.id == todo.id }) else {
159+
return []
160+
}
161+
state.pendingTask = (todo, index)
162+
state.todos.remove(at: index)
163+
setToast(&state, isPresented: true)
164+
case .tapFilterOption(let option):
165+
state.filterOption = option
166+
case .tapTogglePinned(let todo):
167+
return [.togglePinned(todo)]
168+
case .undoDelete:
169+
guard let (todo, index) = state.pendingTask else { return [] }
170+
state.todos.insert(todo, at: index)
171+
state.pendingTask = nil
172+
default:
173+
break
174+
}
175+
return []
176+
}
177+
178+
func reduceByView(_ action: Action, state: inout State) -> [SideEffect] {
179+
switch action {
180+
case .confirmDelete:
181+
guard let (item, _) = state.pendingTask else {
182+
return []
183+
}
184+
return [.delete(item)]
185+
case .onAppear:
186+
return [.fetch]
187+
case .setScope(let scope):
188+
state.scope = scope
189+
case .setSearchText(let text):
190+
state.searchText = text
191+
case .setToast(let isPresented):
192+
setToast(&state, isPresented: isPresented)
193+
case .upsertTodo(let todo):
194+
return [.upsert(todo)]
195+
default:
196+
break
197+
}
198+
return []
199+
}
200+
201+
func reduceByRun(_ action: Action, state: inout State) -> [SideEffect] {
202+
switch action {
203+
case .didTogglePinned(let todo):
204+
if let index = state.todos.firstIndex(where: { $0.id == todo.id }) {
205+
state.todos[index] = todo
206+
}
207+
case .setLoading(let value):
208+
state.isLoading = value
209+
case .setTodos(let todos):
210+
state.todos = todos
211+
default:
147212
break
148213
}
214+
return []
215+
}
216+
}
217+
218+
// MARK: - Helper Methods
219+
private extension TodoViewModel {
220+
func setAlert(
221+
_ state: inout State,
222+
isPresented: Bool
223+
) {
224+
state.alertTitle = "오류"
225+
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
226+
state.showAlert = isPresented
227+
}
228+
229+
func setToast(
230+
_ state: inout State,
231+
isPresented: Bool
232+
) {
233+
state.toastMessage = "실행 취소"
234+
state.showToast = isPresented
149235
}
150236
}

DevLog/Resource/Localizable.xcstrings

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,6 @@
286286
},
287287
"베타 테스트 참여" : {
288288

289-
},
290-
"불러오기 실패" : {
291-
292289
},
293290
"사용자 설정" : {
294291

DevLog/UI/Home/HomeView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ struct HomeView: View {
123123
TodoView(viewModel: TodoViewModel(
124124
fetchTodosByKindUseCase: container.resolve(FetchTodosByKindUseCase.self),
125125
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
126+
deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self),
126127
kind: todoKind
127128
))
128129
.environmentObject(router)

0 commit comments

Comments
 (0)