Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ final class DomainAssembler: Assembler {
container.register(UpsertTodoUseCase.self) {
UpsertTodoUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(DeleteTodoUseCase.self) {
DeleteTodoUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(AuthSessionUseCase.self) {
AuthSessionUseCaseImpl(container.resolve(AuthSessionRepository.self))
Expand Down
10 changes: 10 additions & 0 deletions DevLog/Domain/UseCase/Todo/Delete/DeleteTodoUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// DeleteTodoUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/12/26.
//

protocol DeleteTodoUseCase {
func execute(_ todoID: String) async throws
}
18 changes: 18 additions & 0 deletions DevLog/Domain/UseCase/Todo/Delete/DeleteTodoUseCaseImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// DeleteTodoUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/12/26.
//

final class DeleteTodoUseCaseImpl: DeleteTodoUseCase {
private let repository: TodoRepository

init(_ repository: TodoRepository) {
self.repository = repository
}

func execute(_ todoID: String) async throws {
try await repository.deleteTodo(todoID)
}
}
214 changes: 150 additions & 64 deletions DevLog/Presentation/ViewModel/TodoViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,137 +14,223 @@ final class TodoViewModel: Store {
let kind: TodoKind
var showEditor: Bool = false
var showAlert: Bool = false
var alertTitle: String = ""
var alertMessage: String = ""
var scope: TodoScope = .title
var filterOption: FilterOption = .create
var isLoading = false
var isLoading: Bool = false
var showToast: Bool = false
var toastMessage: String = ""
var pendingTask: (Todo, Int)?
}

enum FilterOption {
case create, update, day, week, month, year
}

enum Action {
// Modifier
case onAppear, refresh

// User
case tapTogglePinned(Todo)
case refresh
case setAlert(Bool)
case setShowEditor(Bool)
case swipeTodo(Todo)
case tapFilterOption(FilterOption)
case upsertTodo(Todo)
case tapTogglePinned(Todo)
case undoDelete

// Binding
case openEditor
case closeEditor
case closeAlert
// View
case confirmDelete
case onAppear
case setScope(TodoScope)
case setSearchText(String)
case setToast(isPresented: Bool)
case upsertTodo(Todo)

// Call from run
case didFetchTodos([Todo])
case didLoading(Bool)
case didShowAlert(String)
// Run
case didTogglePinned(Todo)
case setLoading(Bool)
case setTodos([Todo])
}

enum SideEffect {
case fetchTodos
case upsertTodo(Todo)
case fetch
case upsert(Todo)
case delete(Todo)
case togglePinned(Todo)
case swipeTodo(Todo)
}

private let fetchTodosByKindUseCase: FetchTodosByKindUseCase
private let upsertTodoUseCase: UpsertTodoUseCase
private let deleteTodoUseCase: DeleteTodoUseCase
@Published private(set) var state: State

init(
fetchTodosByKindUseCase: FetchTodosByKindUseCase,
upsertTodoUseCase: UpsertTodoUseCase,
deleteTodoUseCase: DeleteTodoUseCase,
kind: TodoKind
) {
self.fetchTodosByKindUseCase = fetchTodosByKindUseCase
self.upsertTodoUseCase = upsertTodoUseCase
self.deleteTodoUseCase = deleteTodoUseCase
self.state = State(kind: kind)
}

func reduce(with action: Action) -> [SideEffect] {
var state = self.state
var effects: [SideEffect] = []

switch action {
case .onAppear, .refresh:
return [.fetchTodos]
case .tapTogglePinned(let todo):
return [.togglePinned(todo)]
case .swipeTodo(let todo):
return [.swipeTodo(todo)]
case .tapFilterOption(let option):
state.filterOption = option
case .upsertTodo(let todo):
return [.upsertTodo(todo)]
case .openEditor:
state.showEditor = true
case .closeEditor:
state.showEditor = false
case .closeAlert:
state.showAlert = false
case .setScope(let scope):
state.scope = scope
case .setSearchText(let text):
state.searchText = text
case .didFetchTodos(let todos):
state.todos = todos
case .didLoading(let value):
state.isLoading = value
case .didShowAlert(let message):
state.alertMessage = message
state.showAlert = true
case .didTogglePinned(let todo):
if let index = state.todos.firstIndex(where: { $0.id == todo.id }) {
state.todos[index] = todo
}
case .refresh, .setAlert, .setShowEditor, .swipeTodo, .tapFilterOption, .tapTogglePinned, .undoDelete:
effects = reduceByUser(action, state: &state)

case .confirmDelete, .onAppear, .setScope, .setSearchText, .setToast, .upsertTodo:
effects = reduceByView(action, state: &state)

case .didTogglePinned, .setLoading, .setTodos:
effects = reduceByRun(action, state: &state)
}
return []

self.state = state
return effects
}

func run(_ effect: SideEffect) {
switch effect {
case .fetchTodos:
case .fetch:
Task {
do {
defer { send(.didLoading(false)) }
send(.didLoading(true))
defer { send(.setLoading(false)) }
send(.setLoading(true))
let todos = try await fetchTodosByKindUseCase.execute(state.kind)
send(.didFetchTodos(todos))
send(.setTodos(todos))
} catch {
send(.didShowAlert(error.localizedDescription))
send(.setAlert(true))
}
}
case .upsertTodo(let todo):
case .upsert(let item):
Task {
do {
defer { send(.didLoading(false)) }
send(.didLoading(true))
try await upsertTodoUseCase.execute(todo)
defer { send(.setLoading(false)) }
send(.setLoading(true))
try await upsertTodoUseCase.execute(item)
send(.refresh)
} catch {
send(.didShowAlert(error.localizedDescription))
send(.setAlert(true))
}
}
case .togglePinned(let todo):
case .togglePinned(let item):
Task {
do {
defer { send(.didLoading(false)) }
send(.didLoading(true))
var todo = todo
defer { send(.setLoading(false)) }
send(.setLoading(true))
var todo = item
todo.isPinned.toggle()
try await upsertTodoUseCase.execute(todo)
send(.didTogglePinned(todo))
} catch {
send(.didShowAlert(error.localizedDescription))
send(.setAlert(true))
}
}
case .delete(let item):
Task {
do {
try await deleteTodoUseCase.execute(item.id)
} catch {
send(.setAlert(true))
}
}
}
}
}

// MARK: - Reduce Methods
private extension TodoViewModel {
func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .refresh:
return [.fetch]
case .setAlert(let value):
setAlert(&state, isPresented: value)
case .setShowEditor(let value):
state.showEditor = value
case .swipeTodo(let todo):
guard let index = state.todos.firstIndex(where: { $0.id == todo.id }) else {
return []
}
state.pendingTask = (todo, index)
state.todos.remove(at: index)
setToast(&state, isPresented: true)
case .tapFilterOption(let option):
state.filterOption = option
case .tapTogglePinned(let todo):
return [.togglePinned(todo)]
case .undoDelete:
guard let (todo, index) = state.pendingTask else { return [] }
state.todos.insert(todo, at: index)
state.pendingTask = nil
default:
break
}
return []
}

func reduceByView(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .confirmDelete:
guard let (item, _) = state.pendingTask else {
return []
}
return [.delete(item)]
case .onAppear:
return [.fetch]
case .setScope(let scope):
state.scope = scope
case .setSearchText(let text):
state.searchText = text
case .setToast(let isPresented):
setToast(&state, isPresented: isPresented)
case .upsertTodo(let todo):
return [.upsert(todo)]
default:
break
}
return []
}

func reduceByRun(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .didTogglePinned(let todo):
if let index = state.todos.firstIndex(where: { $0.id == todo.id }) {
state.todos[index] = todo
}
case .setLoading(let value):
state.isLoading = value
case .setTodos(let todos):
state.todos = todos
default:
break
}
return []
}
}

// MARK: - Helper Methods
private extension TodoViewModel {
func setAlert(
_ state: inout State,
isPresented: Bool
) {
state.alertTitle = "오류"
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
state.showAlert = isPresented
}

func setToast(
_ state: inout State,
isPresented: Bool
) {
state.toastMessage = "실행 취소"
state.showToast = isPresented
}
}
3 changes: 0 additions & 3 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,6 @@
},
"베타 테스트 참여" : {

},
"불러오기 실패" : {

},
"사용자 설정" : {

Expand Down
1 change: 1 addition & 0 deletions DevLog/UI/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ struct HomeView: View {
TodoView(viewModel: TodoViewModel(
fetchTodosByKindUseCase: container.resolve(FetchTodosByKindUseCase.self),
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self),
kind: todoKind
))
.environmentObject(router)
Expand Down
Loading