From 0edfd8a8a784a9aa41f3dc2db9dc04cdbbf1b7ff Mon Sep 17 00:00:00 2001 From: opficdev Date: Sat, 9 May 2026 21:17:44 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20MainView=EC=9D=98=20Coordinator?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/RootView.swift | 4 +- DevLog/UI/{Common => Main}/MainView.swift | 230 +++++++++++----------- DevLog/UI/Main/MainViewCoordinator.swift | 61 ++++++ 3 files changed, 172 insertions(+), 123 deletions(-) rename DevLog/UI/{Common => Main}/MainView.swift (68%) create mode 100644 DevLog/UI/Main/MainViewCoordinator.swift diff --git a/DevLog/App/RootView.swift b/DevLog/App/RootView.swift index f201ad0c..84f2c47c 100644 --- a/DevLog/App/RootView.swift +++ b/DevLog/App/RootView.swift @@ -19,9 +19,7 @@ struct RootView: View { if let signIn = viewModel.state.signIn { if signIn { MainView( - viewModel: MainViewModel( - unreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self) - ), + container: container, selectedTab: $selectedMainTab ) } else { diff --git a/DevLog/UI/Common/MainView.swift b/DevLog/UI/Main/MainView.swift similarity index 68% rename from DevLog/UI/Common/MainView.swift rename to DevLog/UI/Main/MainView.swift index 8191e8fc..d712c5c9 100644 --- a/DevLog/UI/Common/MainView.swift +++ b/DevLog/UI/Main/MainView.swift @@ -8,29 +8,32 @@ import SwiftUI struct MainView: View { - @Environment(\.diContainer) var container: DIContainer @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @State var viewModel: MainViewModel - @State private var homeNavigationRouter = NavigationRouter() - @State private var todayNavigationRouter = NavigationRouter() - @State private var todoIdToPresent: TodoIdItem? + @State private var coordinator: MainViewCoordinator @Binding var selectedTab: MainTab + private let container: DIContainer + + init( + container: DIContainer, + selectedTab: Binding + ) { + self.container = container + self._coordinator = State(initialValue: MainViewCoordinator(container: container)) + self._selectedTab = selectedTab + } var body: some View { content .onAppear { - viewModel.send(.onAppear) + coordinator.mainViewModel.send(.onAppear) } .alert( - viewModel.state.alertTitle, - isPresented: Binding( - get: { viewModel.state.showAlert }, - set: { viewModel.send(.setAlert($0)) } - ) + coordinator.mainViewModel.state.alertTitle, + isPresented: mainAlertPresented ) { Button(String(localized: "common_close"), role: .cancel) { } } message: { - Text(viewModel.state.alertMessage) + Text(coordinator.mainViewModel.state.alertMessage) } } @@ -43,21 +46,6 @@ struct MainView: View { } } - private var isCompactLayout: Bool { - horizontalSizeClass == .compact - } - - private var sidebarSelection: Binding { - Binding( - get: { selectedTab }, - set: { tab in - if let tab { - selectedTab = tab - } - } - ) - } - private var tabView: some View { TabView(selection: $selectedTab) { homeView @@ -76,7 +64,7 @@ struct MainView: View { .tabItem { tabLabel(.notification) } - .badge(viewModel.state.unreadPushCount) + .badge(coordinator.mainViewModel.state.unreadPushCount) .tag(MainTab.notification) profileView @@ -106,7 +94,7 @@ struct MainView: View { } detail: { homeRegularDetailView } - .environment(homeNavigationRouter) + .environment(coordinator.homeNavigationRouter) case .today: NavigationSplitView { mainSidebar @@ -115,20 +103,19 @@ struct MainView: View { } detail: { todayRegularDetailView } - .environment(todayNavigationRouter) + .environment(coordinator.todayNavigationRouter) case .notification: - let viewModel = makePushNotificationListViewModel() NavigationSplitView { mainSidebar } content: { PushNotificationListView( - viewModel: viewModel, - todoIdToPresent: $todoIdToPresent, + viewModel: coordinator.pushNotificationListViewModel, + todoIdToPresent: todoIdToPresent, isCompactLayout: isCompactLayout ) } detail: { Group { - if let todoId = todoIdToPresent?.id { + if let todoId = coordinator.todoIdToPresent?.id { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), @@ -184,7 +171,7 @@ struct MainView: View { private func sidebarRow(_ tab: MainTab) -> some View { if tab == .notification { tabLabel(tab) - .badge(viewModel.state.unreadPushCount) + .badge(coordinator.mainViewModel.state.unreadPushCount) .tag(tab) } else { tabLabel(tab) @@ -204,7 +191,7 @@ struct MainView: View { private var homeView: some View { Group { if isCompactLayout { - NavigationStack(path: $homeNavigationRouter.path) { + NavigationStack(path: homeNavigationPath) { homeContentView .navigationDestination(for: HomeRoute.self) { homeRoute in homeDestinationView(homeRoute) @@ -214,42 +201,21 @@ struct MainView: View { homeContentView } } - .environment(homeNavigationRouter) + .environment(coordinator.homeNavigationRouter) } private var homeContentView: some View { HomeView( - viewModel: makeHomeViewModel(), + viewModel: coordinator.homeViewModel, isCompactLayout: isCompactLayout ) } - private func makeHomeViewModel() -> 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) - ) - } - - private var homeDetailPath: Binding<[HomeRoute]> { - Binding( - get: { homeNavigationRouter.detailPath }, - set: { homeNavigationRouter.detailPath = $0 } - ) - } - @ViewBuilder private var homeRegularDetailView: some View { NavigationStack(path: homeDetailPath) { Group { - if let homeRoute = homeNavigationRouter.root { + if let homeRoute = coordinator.homeNavigationRouter.root { homeDestinationView(homeRoute) } else { ContentUnavailableView( @@ -286,35 +252,15 @@ struct MainView: View { Text(item.title) .bold() } - } + } } } - private func makeTodoListViewModel(category: TodoCategory) -> TodoListViewModel { - TodoListViewModel( - fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), - fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self), - undoDeleteTodoUseCase: container.resolve(UndoDeleteTodoUseCase.self), - category: category - ) - } - - private func makeTodoDetailViewModel(todoId: String) -> TodoDetailViewModel { - TodoDetailViewModel( - fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), - todoId: todoId - ) - } - @ViewBuilder private var todayView: some View { Group { if isCompactLayout { - NavigationStack(path: $todayNavigationRouter.path) { + NavigationStack(path: todayNavigationPath) { todayContentView .navigationDestination(for: TodayRoute.self) { todayRoute in todayDestinationView(todayRoute) @@ -324,38 +270,21 @@ struct MainView: View { todayContentView } } - .environment(todayNavigationRouter) + .environment(coordinator.todayNavigationRouter) } private var todayContentView: some View { TodayView( - viewModel: makeTodayViewModel(), + viewModel: coordinator.todayViewModel, isCompactLayout: isCompactLayout ) } - private func makeTodayViewModel() -> TodayViewModel { - TodayViewModel( - fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), - fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - fetchTodayDisplayOptionsUseCase: container.resolve(FetchTodayDisplayOptionsUseCase.self), - updateTodayDisplayOptionsUseCase: container.resolve(UpdateTodayDisplayOptionsUseCase.self) - ) - } - - private var todayDetailPath: Binding<[TodayRoute]> { - Binding( - get: { todayNavigationRouter.detailPath }, - set: { todayNavigationRouter.detailPath = $0 } - ) - } - @ViewBuilder private var todayRegularDetailView: some View { NavigationStack(path: todayDetailPath) { Group { - if let todayRoute = todayNavigationRouter.root { + if let todayRoute = coordinator.todayNavigationRouter.root { todayDestinationView(todayRoute) } else { ContentUnavailableView( @@ -382,32 +311,93 @@ struct MainView: View { private var notificationView: some View { PushNotificationListView( - viewModel: makePushNotificationListViewModel(), - todoIdToPresent: $todoIdToPresent, + viewModel: coordinator.pushNotificationListViewModel, + todoIdToPresent: todoIdToPresent, isCompactLayout: isCompactLayout ) } - private func makePushNotificationListViewModel() -> PushNotificationListViewModel { - PushNotificationListViewModel( - fetchUseCase: container.resolve(FetchPushNotificationsUseCase.self), - deleteUseCase: container.resolve(DeletePushNotificationUseCase.self), - undoDeleteUseCase: container.resolve(UndoDeletePushNotificationUseCase.self), - toggleReadUseCase: container.resolve(TogglePushNotificationReadUseCase.self), - fetchQueryUseCase: container.resolve(FetchPushNotificationQueryUseCase.self), - updateQueryUseCase: container.resolve(UpdatePushNotificationQueryUseCase.self) + private var profileView: some View { + ProfileView(viewModel: coordinator.profileViewModel) + } +} + +private extension MainView { + var isCompactLayout: Bool { + horizontalSizeClass == .compact + } + + var mainAlertPresented: Binding { + Binding( + get: { coordinator.mainViewModel.state.showAlert }, + set: { coordinator.mainViewModel.send(.setAlert($0)) } ) } - private var profileView: some View { - ProfileView(viewModel: ProfileViewModel( - fetchUserDataUseCase: container.resolve(FetchUserDataUseCase.self), + var sidebarSelection: Binding { + Binding( + get: { selectedTab }, + set: { tab in + if let tab { + selectedTab = tab + } + } + ) + } + + var todoIdToPresent: Binding { + Binding( + get: { coordinator.todoIdToPresent }, + set: { coordinator.todoIdToPresent = $0 } + ) + } + + var homeNavigationPath: Binding<[HomeRoute]> { + Binding( + get: { coordinator.homeNavigationRouter.path }, + set: { coordinator.homeNavigationRouter.path = $0 } + ) + } + + var homeDetailPath: Binding<[HomeRoute]> { + Binding( + get: { coordinator.homeNavigationRouter.detailPath }, + set: { coordinator.homeNavigationRouter.detailPath = $0 } + ) + } + + var todayNavigationPath: Binding<[TodayRoute]> { + Binding( + get: { coordinator.todayNavigationRouter.path }, + set: { coordinator.todayNavigationRouter.path = $0 } + ) + } + + var todayDetailPath: Binding<[TodayRoute]> { + Binding( + get: { coordinator.todayNavigationRouter.detailPath }, + set: { coordinator.todayNavigationRouter.detailPath = $0 } + ) + } + + func makeTodoListViewModel(category: TodoCategory) -> TodoListViewModel { + TodoListViewModel( fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), - upsertStatusMessageUseCase: container.resolve(UpsertStatusMessageUseCase.self), - networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), - fetchHeatmapActivityTypesUseCase: container.resolve(FetchHeatmapActivityTypesUseCase.self), - updateHeatmapActivityTypesUseCase: container.resolve(UpdateHeatmapActivityTypesUseCase.self) - )) + fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self), + undoDeleteTodoUseCase: container.resolve(UndoDeleteTodoUseCase.self), + category: category + ) + } + + func makeTodoDetailViewModel(todoId: String) -> TodoDetailViewModel { + TodoDetailViewModel( + fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertUseCase: container.resolve(UpsertTodoUseCase.self), + todoId: todoId + ) } } diff --git a/DevLog/UI/Main/MainViewCoordinator.swift b/DevLog/UI/Main/MainViewCoordinator.swift new file mode 100644 index 00000000..2a31f020 --- /dev/null +++ b/DevLog/UI/Main/MainViewCoordinator.swift @@ -0,0 +1,61 @@ +// +// MainViewCoordinator.swift +// DevLog +// +// Created by opfic on 5/9/26. +// + +import Foundation + +@MainActor +@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? + + init(container: DIContainer) { + 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), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + fetchTodayDisplayOptionsUseCase: container.resolve(FetchTodayDisplayOptionsUseCase.self), + updateTodayDisplayOptionsUseCase: container.resolve(UpdateTodayDisplayOptionsUseCase.self) + ) + self.pushNotificationListViewModel = PushNotificationListViewModel( + fetchUseCase: container.resolve(FetchPushNotificationsUseCase.self), + deleteUseCase: container.resolve(DeletePushNotificationUseCase.self), + undoDeleteUseCase: container.resolve(UndoDeletePushNotificationUseCase.self), + toggleReadUseCase: container.resolve(TogglePushNotificationReadUseCase.self), + fetchQueryUseCase: container.resolve(FetchPushNotificationQueryUseCase.self), + updateQueryUseCase: container.resolve(UpdatePushNotificationQueryUseCase.self) + ) + self.profileViewModel = ProfileViewModel( + fetchUserDataUseCase: container.resolve(FetchUserDataUseCase.self), + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + upsertStatusMessageUseCase: container.resolve(UpsertStatusMessageUseCase.self), + networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), + fetchHeatmapActivityTypesUseCase: container.resolve(FetchHeatmapActivityTypesUseCase.self), + updateHeatmapActivityTypesUseCase: container.resolve(UpdateHeatmapActivityTypesUseCase.self) + ) + } +} From a352039f23c2e950faa285adcc07fd7793cbe181 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sat, 9 May 2026 21:41:57 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=B7=B0=EB=B9=8C=EB=8D=94=20=EC=9A=94=EC=86=8C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Main/MainView.swift | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/DevLog/UI/Main/MainView.swift b/DevLog/UI/Main/MainView.swift index d712c5c9..a7683031 100644 --- a/DevLog/UI/Main/MainView.swift +++ b/DevLog/UI/Main/MainView.swift @@ -23,26 +23,23 @@ struct MainView: View { } var body: some View { - content - .onAppear { - coordinator.mainViewModel.send(.onAppear) - } - .alert( - coordinator.mainViewModel.state.alertTitle, - isPresented: mainAlertPresented - ) { - Button(String(localized: "common_close"), role: .cancel) { } - } message: { - Text(coordinator.mainViewModel.state.alertMessage) + Group { + if isCompactLayout { + tabView + } else { + sidebarView } - } - - @ViewBuilder - private var content: some View { - if isCompactLayout { - tabView - } else { - sidebarView + } + .onAppear { + coordinator.mainViewModel.send(.onAppear) + } + .alert( + coordinator.mainViewModel.state.alertTitle, + isPresented: mainAlertPresented + ) { + Button(String(localized: "common_close"), role: .cancel) { } + } message: { + Text(coordinator.mainViewModel.state.alertMessage) } } From d29708f11b4e03b8c0e84474eccd8138c4f46082 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 10 May 2026 01:34:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20showEditButton=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Main/MainView.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/DevLog/UI/Main/MainView.swift b/DevLog/UI/Main/MainView.swift index a7683031..e0068e3a 100644 --- a/DevLog/UI/Main/MainView.swift +++ b/DevLog/UI/Main/MainView.swift @@ -113,13 +113,12 @@ struct MainView: View { } detail: { Group { if let todoId = coordinator.todoIdToPresent?.id { - TodoDetailView(viewModel: TodoDetailViewModel( - fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), - todoId: todoId, - showEditButton: false - )) + TodoDetailView( + viewModel: makeTodoDetailViewModel( + todoId: todoId, + showEditButton: false + ) + ) .id(todoId) } else { ContentUnavailableView( @@ -388,12 +387,16 @@ private extension MainView { ) } - func makeTodoDetailViewModel(todoId: String) -> TodoDetailViewModel { + func makeTodoDetailViewModel( + todoId: String, + showEditButton: Bool = true + ) -> TodoDetailViewModel { TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), upsertUseCase: container.resolve(UpsertTodoUseCase.self), - todoId: todoId + todoId: todoId, + showEditButton: showEditButton ) } }