diff --git a/DevLog/UI/Common/NavigationRouter.swift b/DevLog/UI/Common/NavigationRouter.swift index 956ab997..bfff3939 100644 --- a/DevLog/UI/Common/NavigationRouter.swift +++ b/DevLog/UI/Common/NavigationRouter.swift @@ -28,7 +28,7 @@ final class NavigationRouter { } } - func show(_ route: Route) { + func replace(with route: Route) { path = [route] } diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index a8199bf1..ec2381e6 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -8,11 +8,9 @@ import SwiftUI struct HomeView: View { - @Environment(\.diContainer) var container: any DIContainer - @Environment(NavigationRouter.self) private var router - @State var viewModel: HomeViewModel - let isCompactLayout: Bool @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) + let coordinator: HomeViewCoordinator + let isCompactLayout: Bool var body: some View { List { @@ -24,79 +22,70 @@ struct HomeView: View { .navigationTitle(String(localized: "nav_home")) .toolbar { toolbar } .sheet(isPresented: Binding( - get: { viewModel.state.reorderTodo }, - set: { viewModel.send(.setPresentation(.reorderTodo, $0)) } + get: { coordinator.viewModel.state.reorderTodo }, + set: { coordinator.viewModel.send(.setPresentation(.reorderTodo, $0)) } )) { TodoManageView( - viewModel: TodoManageViewModel(viewModel.state.preferences), + viewModel: coordinator.makeTodoManageViewModel(), onDismiss: { array in - viewModel.send(.setPresentation(.reorderTodo, false)) + coordinator.viewModel.send(.setPresentation(.reorderTodo, false)) withAnimation { - viewModel.send(.orderTodoCategory(array)) + coordinator.viewModel.send(.orderTodoCategory(array)) } } ) } .sheet(isPresented: Binding( - get: { viewModel.state.showContentPicker }, + get: { coordinator.viewModel.state.showContentPicker }, set: { _, _ in } )) { contentPicker } .fullScreenCover(isPresented: Binding( - get: { viewModel.state.showTodoEditor }, - set: { viewModel.send(.setPresentation(.todoEditor, $0)) } + get: { coordinator.viewModel.state.showTodoEditor }, + set: { coordinator.viewModel.send(.setPresentation(.todoEditor, $0)) } )) { - if let selectedCategory = viewModel.state.selectedTodoCategory { + if let selectedCategory = coordinator.viewModel.state.selectedTodoCategory { TodoEditorView( - viewModel: TodoEditorViewModel( - category: selectedCategory, - fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) - ), - onSubmit: { viewModel.send(.addTodo($0)) } + viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory), + onSubmit: { coordinator.viewModel.send(.addTodo($0)) } ) } } .fullScreenCover(isPresented: Binding( - get: { viewModel.state.showSearchView }, - set: { viewModel.send(.setPresentation(.searchView, $0)) } + get: { coordinator.viewModel.state.showSearchView }, + set: { coordinator.viewModel.send(.setPresentation(.searchView, $0)) } )) { - SearchView(viewModel: SearchViewModel( - fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), - fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), - fetchRecentSearchQueriesUseCase: container.resolve(FetchRecentSearchQueriesUseCase.self), - updateRecentSearchQueriesUseCase: container.resolve(UpdateRecentSearchQueriesUseCase.self) - )) + SearchView(viewModel: coordinator.makeSearchViewModel()) } .alert( - viewModel.state.alertTitle, + coordinator.viewModel.state.alertTitle, isPresented: Binding( - get: { viewModel.state.showAlert }, - set: { viewModel.send(.setAlert(isPresented: $0)) } + get: { coordinator.viewModel.state.showAlert }, + set: { coordinator.viewModel.send(.setAlert(isPresented: $0)) } ) ) { alertButtons } message: { - Text(viewModel.state.alertMessage) + Text(coordinator.viewModel.state.alertMessage) } .toast( isPresented: Binding( - get: { viewModel.state.showToast }, - set: { viewModel.send(.setToast(isPresented: $0)) } + get: { coordinator.viewModel.state.showToast }, + set: { coordinator.viewModel.send(.setToast(isPresented: $0)) } ), duration: 5, - action: { viewModel.send(.undoDeleteWebPage) } + action: { coordinator.viewModel.send(.undoDeleteWebPage) } ) { - Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left") + Label(coordinator.viewModel.state.toastMessage, systemImage: "arrow.uturn.left") .font(.caption) .multilineTextAlignment(.center) } .onAppear { - viewModel.send(.onAppear) + coordinator.viewModel.send(.onAppear) } .overlay { - if viewModel.state.isAppending { + if coordinator.viewModel.state.isAppending { LoadingView() } } @@ -104,36 +93,36 @@ struct HomeView: View { @ViewBuilder private var alertButtons: some View { - switch viewModel.state.alertType { + switch coordinator.viewModel.state.alertType { case .webPageInput: TextField( "https://", text: Binding( - get: { viewModel.state.webPageURLInput }, - set: { viewModel.send(.updateWebPageURLInput($0)) } + get: { coordinator.viewModel.state.webPageURLInput }, + set: { coordinator.viewModel.send(.updateWebPageURLInput($0)) } ) ) .textInputAutocapitalization(.never) .keyboardType(.URL) Button(String(localized: "home_add")) { - viewModel.send(.addWebPage) + coordinator.viewModel.send(.addWebPage) } Button(String(localized: "common_cancel"), role: .cancel) { - viewModel.send(.setAlert(isPresented: false)) + coordinator.viewModel.send(.setAlert(isPresented: false)) } case .invalidURL, .error, .none: Button(String(localized: "common_close"), role: .cancel) { - viewModel.send(.setAlert(isPresented: false)) + coordinator.viewModel.send(.setAlert(isPresented: false)) } } } private var todoSection: some View { Section(content: { - if viewModel.state.isPreferencesLoading { + if coordinator.viewModel.state.isPreferencesLoading { LoadingView() } else { - let preferences = viewModel.state.preferences + let preferences = coordinator.viewModel.state.preferences ForEach(preferences.filter { $0.isVisible }, id: \.id) { item in todoCategoryRow(item) } @@ -146,7 +135,7 @@ struct HomeView: View { .bold() Spacer() Button(action: { - viewModel.send(.setPresentation(.reorderTodo, true)) + coordinator.viewModel.send(.setPresentation(.reorderTodo, true)) }) { Image(systemName: "ellipsis") .font(.title2) @@ -159,9 +148,9 @@ struct HomeView: View { private var recentTodoSection: some View { Section { - if viewModel.state.isRecentTodosLoading { + if coordinator.viewModel.state.isRecentTodosLoading { LoadingView() - } else if viewModel.state.recentTodos.isEmpty { + } else if coordinator.viewModel.state.recentTodos.isEmpty { HStack { Spacer() Text(String(localized: "home_recent_empty")) @@ -169,7 +158,7 @@ struct HomeView: View { Spacer() } } else { - ForEach(viewModel.state.recentTodos, id: \.id) { todo in + ForEach(coordinator.viewModel.state.recentTodos, id: \.id) { todo in recentTodoRow(todo) } } @@ -186,13 +175,13 @@ struct HomeView: View { private var webPageSection: some View { Section { - let webPages = viewModel.state.webPages.filter { !$0.isHidden } - if viewModel.state.isWebPageLoading { + let webPages = coordinator.viewModel.state.webPages.filter { !$0.isHidden } + if coordinator.viewModel.state.isWebPageLoading { LoadingView() .id(UUID()) // id 부여를 통해 렌더링 강제 - } else if viewModel.state.needsWebPageRefresh { + } else if coordinator.viewModel.state.needsWebPageRefresh { Button { - viewModel.send(.refreshWebPages) + coordinator.viewModel.send(.refreshWebPages) } label: { HStack { Spacer() @@ -231,18 +220,18 @@ struct HomeView: View { private var toolbar: some ToolbarContent { ToolbarItem(placement: .topBarTrailing) { Button { - viewModel.send(.setPresentation(.contentPicker, true)) + coordinator.viewModel.send(.setPresentation(.contentPicker, true)) } label: { Image(systemName: "plus") } - .disabled(!viewModel.state.isNetworkConnected) + .disabled(!coordinator.viewModel.state.isNetworkConnected) } if #available(iOS 26.0, *) { ToolbarSpacer(.fixed, placement: .topBarTrailing) } ToolbarItemGroup(placement: .topBarTrailing) { Button { - viewModel.send(.setPresentation(.searchView, true)) + coordinator.viewModel.send(.setPresentation(.searchView, true)) } label: { Image(systemName: "magnifyingglass") } @@ -261,7 +250,7 @@ struct HomeView: View { } } else { Button { - router.show(.category(item)) + coordinator.router.replace(with: .category(item)) } label: { labelImage( text: item.localizedName, @@ -281,7 +270,7 @@ struct HomeView: View { } } else { Button { - router.show(.todo(TodoIdItem(id: item.id))) + coordinator.router.replace(with: .todo(TodoIdItem(id: item.id))) } label: { RecentTodoRow(todo: item) .frame(maxWidth: .infinity, alignment: .leading) @@ -299,7 +288,7 @@ struct HomeView: View { } } else { Button { - router.show(.webPage(item)) + coordinator.router.replace(with: .webPage(item)) } label: { WebItemRow(item: item, showsChevron: false) .frame(maxWidth: .infinity, alignment: .leading) @@ -309,7 +298,7 @@ struct HomeView: View { } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { - viewModel.send(.deleteWebPage(item)) + coordinator.viewModel.send(.deleteWebPage(item)) } label: { Label(String(localized: "common_delete"), systemImage: "trash") } @@ -320,14 +309,14 @@ struct HomeView: View { NavigationStack { List { Section { - if viewModel.state.isPreferencesLoading { + if coordinator.viewModel.state.isPreferencesLoading { LoadingView() } else { - let preferences = viewModel.state.preferences.filter(\.isVisible) + let preferences = coordinator.viewModel.state.preferences.filter(\.isVisible) ForEach(preferences, id: \.id) { item in Button { DispatchQueue.main.async { - viewModel.send(.tapTodoCategory(item.category)) + coordinator.viewModel.send(.tapTodoCategory(item.category)) } } label: { labelImage( @@ -346,7 +335,7 @@ struct HomeView: View { Section { Button { DispatchQueue.main.async { - viewModel.send(.setAlert(isPresented: true, type: .webPageInput)) + coordinator.viewModel.send(.setAlert(isPresented: true, type: .webPageInput)) } } label: { labelImage( @@ -365,7 +354,7 @@ struct HomeView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { Button { - viewModel.send(.setPresentation(.contentPicker, false)) + coordinator.viewModel.send(.setPresentation(.contentPicker, false)) } label: { Image(systemName: "xmark") .bold() diff --git a/DevLog/UI/Home/HomeViewCoordinator.swift b/DevLog/UI/Home/HomeViewCoordinator.swift new file mode 100644 index 00000000..1cf29106 --- /dev/null +++ b/DevLog/UI/Home/HomeViewCoordinator.swift @@ -0,0 +1,66 @@ +// +// HomeViewCoordinator.swift +// DevLog +// +// Created by opfic on 5/10/26. +// + +import Foundation + +@MainActor +@Observable +final class HomeViewCoordinator { + let viewModel: HomeViewModel + let router = NavigationRouter() + private let fetchTodoCategoryPreferencesUseCase: FetchTodoCategoryPreferencesUseCase + private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + private let fetchWebPagesUseCase: FetchWebPagesUseCase + private let fetchTodosUseCase: FetchTodosUseCase + private let fetchRecentSearchQueriesUseCase: FetchRecentSearchQueriesUseCase + private let updateRecentSearchQueriesUseCase: UpdateRecentSearchQueriesUseCase + + init(container: DIContainer) { + let fetchTodoCategoryPreferencesUseCase = container.resolve(FetchTodoCategoryPreferencesUseCase.self) + let fetchWebPagesUseCase = container.resolve(FetchWebPagesUseCase.self) + let fetchTodosUseCase = container.resolve(FetchTodosUseCase.self) + + self.fetchTodoCategoryPreferencesUseCase = fetchTodoCategoryPreferencesUseCase + self.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self) + self.fetchWebPagesUseCase = fetchWebPagesUseCase + self.fetchTodosUseCase = fetchTodosUseCase + self.fetchRecentSearchQueriesUseCase = container.resolve(FetchRecentSearchQueriesUseCase.self) + self.updateRecentSearchQueriesUseCase = container.resolve(UpdateRecentSearchQueriesUseCase.self) + self.viewModel = HomeViewModel( + fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase, + updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self), + addWebPageUseCase: container.resolve(AddWebPageUseCase.self), + deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self), + undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + fetchTodosUseCase: fetchTodosUseCase, + fetchWebPagesUseCase: fetchWebPagesUseCase, + networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self) + ) + } + + func makeTodoManageViewModel() -> TodoManageViewModel { + TodoManageViewModel(viewModel.state.preferences) + } + + func makeTodoEditorViewModel(category: TodoCategory) -> TodoEditorViewModel { + TodoEditorViewModel( + category: category, + fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase, + fetchReferenceItemsUseCase: fetchReferenceItemsUseCase + ) + } + + func makeSearchViewModel() -> SearchViewModel { + SearchViewModel( + fetchWebPagesUseCase: fetchWebPagesUseCase, + fetchTodosUseCase: fetchTodosUseCase, + fetchRecentSearchQueriesUseCase: fetchRecentSearchQueriesUseCase, + updateRecentSearchQueriesUseCase: updateRecentSearchQueriesUseCase + ) + } +} diff --git a/DevLog/UI/Main/MainView.swift b/DevLog/UI/Main/MainView.swift index e0068e3a..4ffcbe88 100644 --- a/DevLog/UI/Main/MainView.swift +++ b/DevLog/UI/Main/MainView.swift @@ -10,6 +10,7 @@ import SwiftUI struct MainView: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var coordinator: MainViewCoordinator + @State private var homeViewCoordinator: HomeViewCoordinator @Binding var selectedTab: MainTab private let container: DIContainer @@ -19,6 +20,7 @@ struct MainView: View { ) { self.container = container self._coordinator = State(initialValue: MainViewCoordinator(container: container)) + self._homeViewCoordinator = State(initialValue: HomeViewCoordinator(container: container)) self._selectedTab = selectedTab } @@ -91,7 +93,7 @@ struct MainView: View { } detail: { homeRegularDetailView } - .environment(coordinator.homeNavigationRouter) + .environment(homeViewCoordinator.router) case .today: NavigationSplitView { mainSidebar @@ -197,12 +199,12 @@ struct MainView: View { homeContentView } } - .environment(coordinator.homeNavigationRouter) + .environment(homeViewCoordinator.router) } private var homeContentView: some View { HomeView( - viewModel: coordinator.homeViewModel, + coordinator: homeViewCoordinator, isCompactLayout: isCompactLayout ) } @@ -211,7 +213,7 @@ struct MainView: View { private var homeRegularDetailView: some View { NavigationStack(path: homeDetailPath) { Group { - if let homeRoute = coordinator.homeNavigationRouter.root { + if let homeRoute = homeViewCoordinator.router.root { homeDestinationView(homeRoute) } else { ContentUnavailableView( @@ -350,15 +352,15 @@ private extension MainView { var homeNavigationPath: Binding<[HomeRoute]> { Binding( - get: { coordinator.homeNavigationRouter.path }, - set: { coordinator.homeNavigationRouter.path = $0 } + get: { homeViewCoordinator.router.path }, + set: { homeViewCoordinator.router.path = $0 } ) } var homeDetailPath: Binding<[HomeRoute]> { Binding( - get: { coordinator.homeNavigationRouter.detailPath }, - set: { coordinator.homeNavigationRouter.detailPath = $0 } + get: { homeViewCoordinator.router.detailPath }, + set: { homeViewCoordinator.router.detailPath = $0 } ) } diff --git a/DevLog/UI/Main/MainViewCoordinator.swift b/DevLog/UI/Main/MainViewCoordinator.swift index 2a31f020..efcdd9f3 100644 --- a/DevLog/UI/Main/MainViewCoordinator.swift +++ b/DevLog/UI/Main/MainViewCoordinator.swift @@ -11,11 +11,9 @@ import Foundation @Observable final class MainViewCoordinator { let mainViewModel: MainViewModel - let homeViewModel: HomeViewModel let todayViewModel: TodayViewModel let pushNotificationListViewModel: PushNotificationListViewModel let profileViewModel: ProfileViewModel - let homeNavigationRouter = NavigationRouter() let todayNavigationRouter = NavigationRouter() var todoIdToPresent: TodoIdItem? @@ -23,17 +21,6 @@ final class MainViewCoordinator { self.mainViewModel = MainViewModel( unreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self) ) - self.homeViewModel = HomeViewModel( - fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self), - addWebPageUseCase: container.resolve(AddWebPageUseCase.self), - deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self), - undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), - fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), - networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self) - ) self.todayViewModel = TodayViewModel( fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), diff --git a/DevLog/UI/Today/TodayView.swift b/DevLog/UI/Today/TodayView.swift index ab7fc181..027d1d63 100644 --- a/DevLog/UI/Today/TodayView.swift +++ b/DevLog/UI/Today/TodayView.swift @@ -166,7 +166,7 @@ struct TodayView: View { } } else { Button { - router.show(.todo(TodoIdItem(id: item.id))) + router.replace(with: .todo(TodoIdItem(id: item.id))) } label: { TodayTodoRow(item: item) .frame(maxWidth: .infinity, alignment: .leading)