diff --git a/DevLog/Presentation/ViewModel/HomeViewModel.swift b/DevLog/Presentation/ViewModel/HomeViewModel.swift index 7451a13..f043a05 100644 --- a/DevLog/Presentation/ViewModel/HomeViewModel.swift +++ b/DevLog/Presentation/ViewModel/HomeViewModel.swift @@ -9,45 +9,35 @@ import Foundation final class HomeViewModel: Store { struct State { - // UI var todoKindPreferences = TodoKind.allCases.map { TodoKindPreference(kind: $0, isVisible: true) } var pinnedTodos: [Todo] = [] var showTodoKindPicker: Bool = false var showTodoEditor: Bool = false var showSearchView: Bool = false var selectedTodoKind: TodoKind? - - // User Input var searchText: String = "" var isSearching: Bool = false var reorderTodo: Bool = false - - // Side Effect UI var isLoading: Bool = false - var toastMessage: String = "" - var showToast: Bool = false + var showAlert: Bool = false + var alertTitle: String = "" + var alertMessage: String = "" } enum Action { - // Life Cycle - case onAppear - - // User case tapTodoKind(TodoKind) - case upsertTodo(Todo) case orderTodoKindPreferences([TodoKindPreference]) - - // Binding - case updateSearching(Bool) - case updateSearchText(String) case setReorderTodo(Bool) case setShowTodoEditor(Bool) case setShowTodoKindPicker(Bool) case setShowSearchView(Bool) - case setShowToast(Bool) - - // Call from run - case didFetchPinnedTodos([Todo]) + case setAlert(Bool) + case onAppear + case updateSearching(Bool) + case updateSearchText(String) + case upsertTodo(Todo) + case fetchPinnedTodos([Todo]) + case setLoading(Bool) } enum SideEffect { @@ -69,53 +59,115 @@ final class HomeViewModel: Store { func reduce(with action: Action) -> [SideEffect] { var state = self.state + var effects: [SideEffect] = [] + switch action { - case .onAppear: - return [.fetchPinnedTodos] - case .tapTodoKind(let kind): - state.selectedTodoKind = kind - state.showTodoKindPicker = false - state.showTodoEditor = true - case .updateSearching(let value): - state.isSearching = value - case .updateSearchText(let value): - state.searchText = value - case .setReorderTodo(let value): - state.reorderTodo = value - case .setShowTodoEditor(let value): - state.showTodoEditor = value - if !value { - state.selectedTodoKind = nil - } - case .setShowTodoKindPicker(let value): - state.showTodoKindPicker = value - case .setShowSearchView(let value): - state.showSearchView = value - case .setShowToast(let value): - state.showToast = value - case .upsertTodo(let value): - return [.upsertTodo(value)] - case .orderTodoKindPreferences(let value): - state.todoKindPreferences = value - case .didFetchPinnedTodos(let todos): - state.pinnedTodos = todos + case .tapTodoKind, .orderTodoKindPreferences, .setReorderTodo, + .setShowTodoEditor, .setShowTodoKindPicker, .setShowSearchView, .setAlert: + effects = reduceByUser(action, state: &state) + + case .onAppear, .updateSearching, .updateSearchText, .upsertTodo: + effects = reduceByView(action, state: &state) + + case .fetchPinnedTodos, .setLoading: + effects = reduceByRun(action, state: &state) } self.state = state - return [] + return effects } func run(_ effect: SideEffect) { switch effect { case .upsertTodo(let todo): Task { - try await upsertTodoUseCase.execute(todo) + do { + defer { send(.setLoading(false)) } + send(.setLoading(true)) + try await upsertTodoUseCase.execute(todo) + } catch { + send(.setAlert(true)) + } } case .fetchPinnedTodos: Task { - let todos = try await fetchPinnedTodosUseCase.execute() - send(.didFetchPinnedTodos(todos)) + do { + defer { send(.setLoading(false)) } + send(.setLoading(true)) + let todos = try await fetchPinnedTodosUseCase.execute() + send(.fetchPinnedTodos(todos)) + } catch { + send(.setAlert(true)) + } } } } } + +// MARK: - Reduce Methods +private extension HomeViewModel { + func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] { + switch action { + case .tapTodoKind(let kind): + state.selectedTodoKind = kind + state.showTodoKindPicker = false + state.showTodoEditor = true + case .orderTodoKindPreferences(let preferences): + state.todoKindPreferences = preferences + case .setReorderTodo(let isPresented): + state.reorderTodo = isPresented + case .setShowTodoEditor(let isPresented): + state.showTodoEditor = isPresented + if !isPresented { state.selectedTodoKind = nil } + case .setShowTodoKindPicker(let isPresented): + state.showTodoKindPicker = isPresented + case .setShowSearchView(let isPresented): + state.showSearchView = isPresented + case .setAlert(let isPresented): + setAlert(&state, isPresented: isPresented) + default: + break + } + return [] + } + + func reduceByView(_ action: Action, state: inout State) -> [SideEffect] { + switch action { + case .onAppear: + return [.fetchPinnedTodos] + case .updateSearching(let isSearching): + state.isSearching = isSearching + case .updateSearchText(let text): + state.searchText = text + case .upsertTodo(let todo): + return [.upsertTodo(todo)] + default: + break + } + return [] + } + + func reduceByRun(_ action: Action, state: inout State) -> [SideEffect] { + switch action { + case .fetchPinnedTodos(let todos): + state.pinnedTodos = todos + case .setLoading(let isLoading): + state.isLoading = isLoading + default: + break + } + return [] + } +} + +// MARK: - Helper Methods +private extension HomeViewModel { + func setAlert( + _ state: inout State, + isPresented: Bool + ) { + state.alertTitle = "오류" + state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요." + state.showAlert = isPresented + } +} diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index 9a3979b..0155c93 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -78,26 +78,20 @@ struct HomeView: View { fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self) )) } - .alert("", isPresented: Binding( - get: { viewModel.state.showToast }, - set: { _, _ in }) + .alert( + viewModel.state.alertTitle, + isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { viewModel.send(.setAlert($0)) } + ) ) { - Button(action: { - viewModel.send(.setShowToast(false)) - }) { - Text("확인") - } + Button("확인", role: .cancel) { } } message: { - Text(viewModel.state.toastMessage) + Text(viewModel.state.alertMessage) } .onAppear { viewModel.send(.onAppear) } - .overlay { - if viewModel.state.isLoading { - LoadingView() - } - } } } @@ -144,11 +138,15 @@ struct HomeView: View { private var pinnedSection: some View { Section(content: { if viewModel.state.pinnedTodos.isEmpty { - HStack { - Spacer() - Text("최근에 중요 표시를 한 Todo가 여기 표시됩니다.") - .font(.callout) - Spacer() + if viewModel.state.isLoading { + LoadingView() + } else { + HStack { + Spacer() + Text("최근에 중요 표시를 한 Todo가 여기 표시됩니다.") + .font(.callout) + Spacer() + } } } else { ForEach(viewModel.state.pinnedTodos, id: \.id) { todo in