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 62% rename from DevLog/UI/Common/MainView.swift rename to DevLog/UI/Main/MainView.swift index 8191e8fc..e0068e3a 100644 --- a/DevLog/UI/Common/MainView.swift +++ b/DevLog/UI/Main/MainView.swift @@ -8,54 +8,39 @@ 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) - } - .alert( - viewModel.state.alertTitle, - isPresented: Binding( - get: { viewModel.state.showAlert }, - set: { viewModel.send(.setAlert($0)) } - ) - ) { - Button(String(localized: "common_close"), role: .cancel) { } - } message: { - Text(viewModel.state.alertMessage) + Group { + if isCompactLayout { + tabView + } else { + sidebarView } - } - - @ViewBuilder - private var content: some View { - if isCompactLayout { - tabView - } else { - sidebarView } - } - - private var isCompactLayout: Bool { - horizontalSizeClass == .compact - } - - private var sidebarSelection: Binding { - Binding( - get: { selectedTab }, - set: { tab in - if let tab { - selectedTab = tab - } - } - ) + .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) + } } private var tabView: some View { @@ -76,7 +61,7 @@ struct MainView: View { .tabItem { tabLabel(.notification) } - .badge(viewModel.state.unreadPushCount) + .badge(coordinator.mainViewModel.state.unreadPushCount) .tag(MainTab.notification) profileView @@ -106,7 +91,7 @@ struct MainView: View { } detail: { homeRegularDetailView } - .environment(homeNavigationRouter) + .environment(coordinator.homeNavigationRouter) case .today: NavigationSplitView { mainSidebar @@ -115,27 +100,25 @@ 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 { - TodoDetailView(viewModel: TodoDetailViewModel( - fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), - todoId: todoId, - showEditButton: false - )) + if let todoId = coordinator.todoIdToPresent?.id { + TodoDetailView( + viewModel: makeTodoDetailViewModel( + todoId: todoId, + showEditButton: false + ) + ) .id(todoId) } else { ContentUnavailableView( @@ -184,7 +167,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 +187,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 +197,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 +248,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 +266,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 +307,97 @@ 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, + showEditButton: Bool = true + ) -> TodoDetailViewModel { + TodoDetailViewModel( + fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertUseCase: container.resolve(UpsertTodoUseCase.self), + todoId: todoId, + showEditButton: showEditButton + ) } } 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) + ) + } +}