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
200 changes: 106 additions & 94 deletions DevLog/Presentation/ViewModel/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ final class HomeViewModel: Store {
var showSearchView: Bool = false
var webPageURLInput: String = "https://"
var selectedTodoKind: TodoKind?
var searchText: String = ""
var isSearching: Bool = false
var reorderTodo: Bool = false
var isRecentTodosLoading: Bool = false
var isWebPageLoading: Bool = false
Expand All @@ -34,28 +32,21 @@ final class HomeViewModel: Store {
}

enum Action {
case onAppear
case setPresentation(Presentation, Bool)
case setAlert(isPresented: Bool, type: AlertType? = nil)
case setToast(isPresented: Bool, type: ToastType? = nil)
case setLoading(LoadingTarget, Bool)
case tapTodoKind(TodoKind)
case orderTodoKindPreferences([TodoKindPreference])
case setReorderTodo(Bool)
case setShowTodoEditor(Bool)
case setShowContentPicker(Bool)
case setShowSearchView(Bool)
case setAlert(isPresented: Bool, type: AlertType? = nil)
case onAppear
case updateWebPageURLInput(String)
case updateSearching(Bool)
case updateSearchText(String)
case addTodo(Todo)
case updateRecentTodos([RecentTodoItem])
case updateWebPageURLInput(String)
case addWebPage
case deleteWebPage(WebPageItem)
case undoDeleteWebPage
case setToast(isPresented: Bool, type: ToastType? = nil)
case fetchRecentTodos([RecentTodoItem])
case fetchWebPages([WebPageItem])
case updateWebPages([WebPageItem])
case restoreWebPage(WebPageItem, Int)
case setRecentTodosLoading(Bool)
case setWebPageLoading(Bool)
case setAppending(Bool)
}

enum SideEffect {
Expand Down Expand Up @@ -83,6 +74,19 @@ final class HomeViewModel: Store {
case urlInputAlert
}

enum Presentation {
case reorderTodo
case todoEditor
case contentPicker
case searchView
}

enum LoadingTarget {
case recentTodos
case webPage
case overlay
}

private(set) var state = State()
private let upsertTodoUseCase: UpsertTodoUseCase
private let addWebPageUseCase: AddWebPageUseCase
Expand Down Expand Up @@ -113,17 +117,12 @@ final class HomeViewModel: Store {
var effects: [SideEffect] = []

switch action {
case .tapTodoKind, .orderTodoKindPreferences, .setReorderTodo,
.setShowTodoEditor, .setShowContentPicker, .setShowSearchView,
.updateWebPageURLInput, .setAlert, .deleteWebPage,
.undoDeleteWebPage, .setToast:
effects = reduceByUser(action, state: &state)

case .onAppear, .updateSearching, .updateSearchText, .addTodo, .addWebPage:
case .onAppear, .setPresentation, .setAlert, .setToast, .tapTodoKind,
.orderTodoKindPreferences, .addTodo, .updateWebPageURLInput,
.addWebPage, .deleteWebPage, .undoDeleteWebPage:
effects = reduceByView(action, state: &state)

case .fetchRecentTodos, .fetchWebPages, .restoreWebPage, .setRecentTodosLoading,
.setWebPageLoading, .setAppending:
case .setLoading, .updateRecentTodos, .updateWebPages, .restoreWebPage:
effects = reduceByRun(action, state: &state)
}

Expand All @@ -136,50 +135,51 @@ final class HomeViewModel: Store {
case .addTodo(let todo):
Task {
do {
send(.setAppending(true))
defer { send(.setLoading(.overlay, false)) }
send(.setLoading(.overlay, true))
try await upsertTodoUseCase.execute(todo)
let page = try await fetchRecentTodos()
let items = page.items
.filter { $0.createdAt != $0.updatedAt }
.prefix(5)
.map { RecentTodoItem(from: $0) }
send(.fetchRecentTodos(items))
send(.updateRecentTodos(items))
} catch {
send(.setAlert(isPresented: true, type: .error))
}
}
case .fetchRecentTodos:
Task {
do {
defer { send(.setRecentTodosLoading(false)) }
send(.setRecentTodosLoading(true))
defer { send(.setLoading(.recentTodos, false)) }
send(.setLoading(.recentTodos, true))
let page = try await fetchRecentTodos()
let items = page.items
.filter { $0.createdAt != $0.updatedAt }
.prefix(5)
.map { RecentTodoItem(from: $0) }
send(.fetchRecentTodos(items))
send(.updateRecentTodos(items))
} catch {
send(.setAlert(isPresented: true, type: .error))
}
}
Comment on lines 135 to 165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

.addTodo.fetchRecentTodos 사이드 이펙트 처리 로직에 중복 코드가 있습니다. 최근 Todo 목록을 가져와서 상태를 업데이트하는 로직을 별도의 private 메서드로 추출하여 중복을 제거하고 코드 유지보수성을 높이는 것을 제안합니다.

예를 들어, 다음과 같은 private helper 메서드를 추가할 수 있습니다.

private func refreshRecentTodos() async throws {
    let page = try await fetchRecentTodos()
    let items = page.items
        .filter { $0.createdAt != $0.updatedAt }
        .prefix(5)
        .map { RecentTodoItem(from: $0) }
    send(.updateRecentTodos(items))
}

그리고 이 메서드를 사용하여 .addTodo.fetchRecentTodos 케이스를 리팩토링할 수 있습니다.

        case .addTodo(let todo):
            Task {
                do {
                    defer { send(.setLoading(.overlay, false)) }
                    send(.setLoading(.overlay, true))
                    try await upsertTodoUseCase.execute(todo)
                    try await refreshRecentTodos()
                } catch {
                    send(.setAlert(isPresented: true, type: .error))
                }
            }
        case .fetchRecentTodos:
            Task {
                do {
                    defer { send(.setLoading(.recentTodos, false)) }
                    send(.setLoading(.recentTodos, true))
                    try await refreshRecentTodos()
                } catch {
                    send(.setAlert(isPresented: true, type: .error))
                }
            }

case .addWebPage(let urlString):
Task {
do {
defer { send(.setAppending(false)) }
send(.setAppending(true))
defer { send(.setLoading(.overlay, false)) }
send(.setLoading(.overlay, true))
try await addWebPageUseCase.execute(urlString)
let pages = try await fetchWebPagesUseCase.execute("")
send(.fetchWebPages(pages.map { WebPageItem(from: $0) }))
send(.updateWebPages(pages.map { WebPageItem(from: $0) }))
} catch {
send(.setAlert(isPresented: true, type: .error))
}
}
case .deleteWebPage(let page, let index):
Task {
do {
defer { send(.setWebPageLoading(false)) }
send(.setWebPageLoading(true))
defer { send(.setLoading(.webPage, false)) }
send(.setLoading(.webPage, true))
try await deleteWebPageUseCase.execute(page.url.absoluteString)
} catch {
send(.restoreWebPage(page, index))
Expand All @@ -188,8 +188,8 @@ final class HomeViewModel: Store {
}
case .undoDeleteWebPage(let urlString):
Task {
defer { send(.setWebPageLoading(false)) }
send(.setWebPageLoading(true))
defer { send(.setLoading(.webPage, false)) }
send(.setLoading(.webPage, true))

var shouldPresentError = false

Expand All @@ -201,7 +201,7 @@ final class HomeViewModel: Store {

do {
let pages = try await fetchWebPagesUseCase.execute("")
send(.fetchWebPages(pages.map { WebPageItem(from: $0) }))
send(.updateWebPages(pages.map { WebPageItem(from: $0) }))
} catch {
shouldPresentError = true
}
Expand All @@ -213,10 +213,10 @@ final class HomeViewModel: Store {
case .fetchWebPages:
Task {
do {
defer { send(.setWebPageLoading(false)) }
send(.setWebPageLoading(true))
defer { send(.setLoading(.webPage, false)) }
send(.setLoading(.webPage, true))
let pages = try await fetchWebPagesUseCase.execute("")
send(.fetchWebPages(pages.map { WebPageItem(from: $0) }))
send(.updateWebPages(pages.map { WebPageItem(from: $0) }))
} catch {
send(.setAlert(isPresented: true, type: .error))
}
Expand All @@ -226,7 +226,7 @@ final class HomeViewModel: Store {
try await Task.sleep(for: .seconds(0.1))
switch type {
case .todoEditor:
send(.setShowTodoEditor(true))
send(.setPresentation(.todoEditor, true))
case .urlInputAlert:
send(.setAlert(isPresented: true, type: .webPageInput))
}
Expand All @@ -237,81 +237,66 @@ final class HomeViewModel: Store {

// MARK: - Reduce Methods
private extension HomeViewModel {
func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] {
// swiftlint:disable cyclomatic_complexity
func reduceByView(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .tapTodoKind(let kind):
state.selectedTodoKind = kind
state.showContentPicker = false
return [.showModalAfterDelay(.todoEditor)]
case .orderTodoKindPreferences(let preferences):
state.todoKindPreferences = preferences
case .setReorderTodo(let presented):
state.reorderTodo = presented
case .setShowTodoEditor(let presented):
state.showTodoEditor = presented
if !presented { state.selectedTodoKind = nil }
case .setShowContentPicker(let presented):
state.showContentPicker = presented
case .setShowSearchView(let presented):
state.showSearchView = presented
case .updateWebPageURLInput(let text):
state.webPageURLInput = text
case .onAppear:
return [.fetchRecentTodos, .fetchWebPages]
case .setPresentation(let presentation, let isPresented):
setPresentation(&state, presentation: presentation, isPresented: isPresented)
case .setAlert(let presented, let type):
if presented && type == .webPageInput && state.showContentPicker {
state.showContentPicker = false
return [.showModalAfterDelay(.urlInputAlert)]
}
setAlert(&state, isPresented: presented, type: type)
case .deleteWebPage(let page):
if let index = state.webPages.firstIndex(where: { $0.id == page.id }) {
deletedWebPageURLString = page.url.absoluteString
state.webPages.remove(at: index)
setToast(&state, isPresented: true, for: .deleteWebPage)
return [.deleteWebPage(page, index)]
}
case .undoDeleteWebPage:
guard let deletedWebPageURLString else { return [] }
self.deletedWebPageURLString = nil
return [.undoDeleteWebPage(deletedWebPageURLString)]
case .setToast(let isPresented, let type):
setToast(&state, isPresented: isPresented, for: type)
if !isPresented {
deletedWebPageURLString = nil
}
default:
break
}
return []
}

func reduceByView(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .onAppear:
return [.fetchRecentTodos, .fetchWebPages]
case .updateSearching(let isSearching):
state.isSearching = isSearching
case .updateSearchText(let text):
state.searchText = text
case .tapTodoKind(let kind):
state.selectedTodoKind = kind
state.showContentPicker = false
return [.showModalAfterDelay(.todoEditor)]
case .orderTodoKindPreferences(let preferences):
state.todoKindPreferences = preferences
case .addTodo(let todo):
return [.addTodo(todo)]
case .updateWebPageURLInput(let text):
state.webPageURLInput = text
case .addWebPage:
guard let normalizedURL = normalizedWebPageURL(state.webPageURLInput) else {
setAlert(&state, isPresented: true, type: .invalidURL)
return []
}
setAlert(&state, isPresented: false, type: nil)
return [.addWebPage(normalizedURL)]
case .deleteWebPage(let page):
if let index = state.webPages.firstIndex(where: { $0.id == page.id }) {
deletedWebPageURLString = page.url.absoluteString
state.webPages.remove(at: index)
setToast(&state, isPresented: true, for: .deleteWebPage)
return [.deleteWebPage(page, index)]
}
case .undoDeleteWebPage:
guard let deletedWebPageURLString else { return [] }
self.deletedWebPageURLString = nil
return [.undoDeleteWebPage(deletedWebPageURLString)]
default:
break
}
return []
}
// swiftlint:enable cyclomatic_complexity

func reduceByRun(_ action: Action, state: inout State) -> [SideEffect] {
switch action {
case .fetchRecentTodos(let todos):
case .setLoading(let loadingTarget, let isLoading):
setLoading(&state, loadingTarget: loadingTarget, isLoading: isLoading)
case .updateRecentTodos(let todos):
state.recentTodos = todos
case .fetchWebPages(let pages):
case .updateWebPages(let pages):
state.webPages = pages
case .restoreWebPage(let page, let index):
if state.webPages.contains(where: { $0.id == page.id }) { break }
Expand All @@ -323,12 +308,6 @@ private extension HomeViewModel {
if deletedWebPageURLString == page.url.absoluteString {
deletedWebPageURLString = nil
}
case .setRecentTodosLoading(let isLoading):
state.isRecentTodosLoading = isLoading
case .setWebPageLoading(let isLoading):
state.isWebPageLoading = isLoading
case .setAppending(let isLoading):
state.isAppending = isLoading
default:
break
}
Expand All @@ -338,6 +317,24 @@ private extension HomeViewModel {

// MARK: - Helper Methods
private extension HomeViewModel {
func setPresentation(
_ state: inout State,
presentation: Presentation,
isPresented: Bool
) {
switch presentation {
case .reorderTodo:
state.reorderTodo = isPresented
case .todoEditor:
state.showTodoEditor = isPresented
if !isPresented { state.selectedTodoKind = nil }
case .contentPicker:
state.showContentPicker = isPresented
case .searchView:
state.showSearchView = isPresented
}
}

func setAlert(
_ state: inout State,
isPresented: Bool,
Expand Down Expand Up @@ -377,6 +374,21 @@ private extension HomeViewModel {
state.toastType = type
}

func setLoading(
_ state: inout State,
loadingTarget: LoadingTarget,
isLoading: Bool
) {
switch loadingTarget {
case .recentTodos:
state.isRecentTodosLoading = isLoading
case .webPage:
state.isWebPageLoading = isLoading
case .overlay:
state.isAppending = isLoading
}
}

func normalizedWebPageURL(_ input: String) -> String? {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
Expand Down
Loading