diff --git a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings index 720e6cea..fba808af 100644 --- a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings +++ b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings @@ -966,6 +966,23 @@ } } }, + "profile_select_detail" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select an activity." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활동을 선택해주세요." + } + } + } + }, "profile_select_quarter" : { "extractionState" : "manual", "localizations" : { @@ -3451,4 +3468,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index fef09bd7..77287698 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -106,65 +106,62 @@ struct MainView: View { @ViewBuilder private func sidebarView(for selectedTab: MainTab) -> some View { - switch selectedTab.mainTabSplitStyle { - case .detailOnly: + switch selectedTab { + case .home: NavigationSplitView { mainSidebar + } content: { + homeView + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) } detail: { - selectedTabView(for: selectedTab) + homeRegularDetailView } - case .contentDetail: - switch selectedTab { - case .home: - NavigationSplitView { - mainSidebar - } content: { - homeView - } detail: { - homeRegularDetailView - } - .environment(homeViewCoordinator.router) - case .today: - NavigationSplitView { - mainSidebar - } content: { - todayView - } detail: { - todayRegularDetailView - } - case .notification: - NavigationSplitView { - mainSidebar - } content: { - PushNotificationListView( - coordinator: pushNotificationListViewCoordinator, - isCompactLayout: isCompactLayout - ) - } detail: { - Group { - if let todoId = pushNotificationListViewCoordinator.todoIdToPresent?.id { - TodoDetailView( - viewModel: pushNotificationListViewCoordinator.makeTodoDetailViewModel( - todoId: todoId - ) - ) - .id(todoId) - } else { - ContentUnavailableView( - String(localized: "push_notifications_select_detail"), - systemImage: "bell.badge" + .environment(homeViewCoordinator.router) + case .today: + NavigationSplitView { + mainSidebar + } content: { + todayView + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) + } detail: { + todayRegularDetailView + } + case .notification: + NavigationSplitView { + mainSidebar + } content: { + PushNotificationListView( + coordinator: pushNotificationListViewCoordinator, + isCompactLayout: isCompactLayout + ) + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) + } detail: { + Group { + if let todoId = pushNotificationListViewCoordinator.todoIdToPresent?.id { + TodoDetailView( + viewModel: pushNotificationListViewCoordinator.makeTodoDetailViewModel( + todoId: todoId ) - } + ) + .id(todoId) + } else { + ContentUnavailableView( + String(localized: "push_notifications_select_detail"), + systemImage: "bell.badge" + ) } - .background(Color(.secondarySystemBackground).ignoresSafeArea()) - } - case .profile: - NavigationSplitView { - mainSidebar - } detail: { - selectedTabView(for: selectedTab) } } + .background(Color(.secondarySystemBackground).ignoresSafeArea()) + case .profile: + NavigationSplitView { + mainSidebar + } content: { + profileView + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) + } detail: { + profileRegularDetailView + } } } @@ -178,20 +175,6 @@ struct MainView: View { .listStyle(.sidebar) } - @ViewBuilder - private func selectedTabView(for selectedTab: MainTab) -> some View { - switch selectedTab { - case .home: - homeView - case .today: - todayView - case .notification: - notificationView - case .profile: - profileView - } - } - @ViewBuilder private func sidebarRow(_ tab: MainTab) -> some View { if tab == .notification { @@ -341,7 +324,57 @@ struct MainView: View { } private var profileView: some View { - ProfileView(coordinator: profileViewCoordinator) + ProfileView( + coordinator: profileViewCoordinator, + isCompactLayout: isCompactLayout + ) + } + + private var profileRegularDetailView: some View { + NavigationStack(path: Binding( + get: { profileViewCoordinator.router.detailPath }, + set: { profileViewCoordinator.router.detailPath = $0 } + )) { + Group { + if let profileRoute = profileViewCoordinator.router.root { + profileRegularDestinationView(profileRoute) + } else { + ContentUnavailableView( + String(localized: "profile_select_detail"), + systemImage: "person.crop.circle" + ) + } + } + .navigationDestination(for: ProfileRoute.self) { route in + profileRegularDestinationView(route) + } + } + .background(Color(.secondarySystemBackground).ignoresSafeArea()) + } + + @ViewBuilder + private func profileRegularDestinationView(_ route: ProfileRoute) -> some View { + switch route { + case .activity(let todoId): + TodoDetailView(viewModel: profileViewCoordinator.makeTodoDetailViewModel(todoId: todoId)) + .id(todoId) + case .settings: + SettingView(viewModel: profileViewCoordinator.settingViewModel) + .environment(profileViewCoordinator.router) + case .theme: + ThemeView( + theme: Binding( + get: { profileViewCoordinator.settingViewModel.state.theme }, + set: { profileViewCoordinator.settingViewModel.send(.setTheme($0)) } + ) + ) + case .pushNotification: + PushNotificationSettingsView( + viewModel: profileViewCoordinator.makePushNotificationSettingsViewModel() + ) + case .account: + AccountView(viewModel: profileViewCoordinator.makeAccountViewModel()) + } } } @@ -397,22 +430,7 @@ private extension MainView { } } - -private enum MainTabSplitStyle { - case detailOnly - case contentDetail -} - private extension MainTab { - var mainTabSplitStyle: MainTabSplitStyle { - switch self { - case .home, .today, .notification: - .contentDetail - case .profile: - .detailOnly - } - } - var title: String { switch self { case .home: diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift index b19f37d4..25727f13 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift @@ -11,137 +11,158 @@ import DevLogDomain struct ProfileView: View { let coordinator: ProfileViewCoordinator + let isCompactLayout: Bool @FocusState private var focused: Bool var body: some View { - NavigationStack(path: navigationPath) { - ScrollView { - LazyVStack(alignment: .leading, spacing: 16) { - HStack { - CacheableImage(url: coordinator.viewModel.state.avatarURL) { - Image(systemName: "person.crop.circle.fill") - .resizable() - .scaledToFill() - .foregroundStyle(Color(.systemGray2)) + Group { + if isCompactLayout { + NavigationStack(path: navigationPath) { + profileContentView + .navigationDestination(for: ProfileRoute.self) { route in + profileDestinationView(route) } - .frame(width: 60, height: 60) - .cornerRadius(30) - .foregroundStyle(Color.gray) + } + } else { + profileContentView + } + } + .toolbar { profileToolbarContent } + .onChange(of: focused) { _, newValue in + withAnimation { + coordinator.viewModel.send(.updateStatusTextFieldFocus(newValue)) + } + } + .alert( + "", + isPresented: Binding( + get: { coordinator.viewModel.state.showAlert }, + set: { coordinator.viewModel.send(.setAlert($0)) } + ) + ) { + Button(String(localized: "common_close"), role: .cancel) { } + } message: { + Text(coordinator.viewModel.state.alertMessage) + } + .sheet( + isPresented: Binding( + get: { coordinator.viewModel.state.showQuarterPicker }, + set: { coordinator.viewModel.send(.setQuarterPickerPresented($0)) } + ) + ) { + quarterPickerSheet + } + } - VStack(alignment: .leading) { - Text(coordinator.viewModel.state.name) - .font(.title2) - .bold() - Text(coordinator.viewModel.state.email) - .font(.caption2) - .foregroundStyle(Color.gray) - } + private var profileContentView: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 16) { + HStack { + CacheableImage(url: coordinator.viewModel.state.avatarURL) { + Image(systemName: "person.crop.circle.fill") + .resizable() + .scaledToFill() + .foregroundStyle(Color(.systemGray2)) } - let connected = coordinator.viewModel.state.isNetworkConnected - HStack { - HStack { - Image(systemName: "face.smiling") - TextField( - text: Binding( - get: { coordinator.viewModel.state.statusMessage }, - set: { coordinator.viewModel.send(.updateStatusMessage($0)) } - ) - ) { - Text(String(localized: "profile_status_placeholder")) - } - .frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight) - .focused($focused) - .disabled(!connected) + .frame(width: 60, height: 60) + .cornerRadius(30) + .foregroundStyle(Color.gray) - if !coordinator.viewModel.state.statusMessage.isEmpty, - coordinator.viewModel.state.showDoneButton { - Button(action: { - coordinator.viewModel.send(.tapResetStatusMessageButton) - }) { - Image(systemName: "xmark.circle.fill") - } - .transition(.move(edge: .trailing).combined(with: .opacity)) - } + VStack(alignment: .leading) { + Text(coordinator.viewModel.state.name) + .font(.title2) + .bold() + Text(coordinator.viewModel.state.email) + .font(.caption2) + .foregroundStyle(Color.gray) + } + } + let connected = coordinator.viewModel.state.isNetworkConnected + HStack { + HStack { + Image(systemName: "face.smiling") + TextField( + text: Binding( + get: { coordinator.viewModel.state.statusMessage }, + set: { coordinator.viewModel.send(.updateStatusMessage($0)) } + ) + ) { + Text(String(localized: "profile_status_placeholder")) } - .foregroundStyle(Color.gray) - .padding(8) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(Color(.secondarySystemGroupedBackground)) - ) - if coordinator.viewModel.state.showDoneButton { + .frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight) + .focused($focused) + .disabled(!connected) + + if !coordinator.viewModel.state.statusMessage.isEmpty, + coordinator.viewModel.state.showDoneButton { Button(action: { - focused = false - coordinator.viewModel.send(.willUpdateStatusMessage) + coordinator.viewModel.send(.tapResetStatusMessageButton) }) { - Text(String(localized: "profile_done")) + Image(systemName: "xmark.circle.fill") } .transition(.move(edge: .trailing).combined(with: .opacity)) } } - .opacity(connected ? 1 : 0.7) - activityHeatmapSection - } - .padding(.horizontal, 16) - } - .refreshable { coordinator.viewModel.send(.refresh) } - .frame(maxWidth: .infinity) - .background(Color(.systemGroupedBackground)) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - HStack(spacing: 0) { - Button { - coordinator.router.push(.settings) - } label: { - Image(systemName: "gearshape") + .foregroundStyle(Color.gray) + .padding(8) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color(.secondarySystemGroupedBackground)) + ) + if coordinator.viewModel.state.showDoneButton { + Button(action: { + focused = false + coordinator.viewModel.send(.willUpdateStatusMessage) + }) { + Text(String(localized: "profile_done")) } + .transition(.move(edge: .trailing).combined(with: .opacity)) } } + .opacity(connected ? 1 : 0.7) + activityHeatmapSection } - .navigationDestination(for: ProfileRoute.self) { route in - switch route { - case .settings: - SettingView(viewModel: coordinator.settingViewModel) - .environment(coordinator.router) - case .activity(let todoId): - TodoDetailView(viewModel: coordinator.makeTodoDetailViewModel(todoId: todoId)) - case .theme: - ThemeView( - theme: Binding( - get: { coordinator.settingViewModel.state.theme }, - set: { coordinator.settingViewModel.send(.setTheme($0)) } - ) - ) - case .pushNotification: - PushNotificationSettingsView(viewModel: coordinator.makePushNotificationSettingsViewModel()) - case .account: - AccountView(viewModel: coordinator.makeAccountViewModel()) - } - } - .onChange(of: focused) { _, newValue in - withAnimation { - coordinator.viewModel.send(.updateStatusTextFieldFocus(newValue)) + .padding(.horizontal, 16) + } + .refreshable { coordinator.viewModel.send(.refresh) } + .frame(maxWidth: .infinity) + .background(Color(.systemGroupedBackground)) + } + + @ToolbarContentBuilder + private var profileToolbarContent: some ToolbarContent { + ToolbarItem(placement: .topBarTrailing) { + Button { + if isCompactLayout { + coordinator.router.push(.settings) + } else { + coordinator.router.replace(with: .settings) } + } label: { + Image(systemName: "gearshape") } - .alert( - "", - isPresented: Binding( - get: { coordinator.viewModel.state.showAlert }, - set: { coordinator.viewModel.send(.setAlert($0)) } - ) - ) { - Button(String(localized: "common_close"), role: .cancel) { } - } message: { - Text(coordinator.viewModel.state.alertMessage) - } - .sheet( - isPresented: Binding( - get: { coordinator.viewModel.state.showQuarterPicker }, - set: { coordinator.viewModel.send(.setQuarterPickerPresented($0)) } + } + } + + @ViewBuilder + private func profileDestinationView(_ route: ProfileRoute) -> some View { + switch route { + case .settings: + SettingView(viewModel: coordinator.settingViewModel) + .environment(coordinator.router) + case .activity(let todoId): + TodoDetailView(viewModel: coordinator.makeTodoDetailViewModel(todoId: todoId)) + case .theme: + ThemeView( + theme: Binding( + get: { coordinator.settingViewModel.state.theme }, + set: { coordinator.settingViewModel.send(.setTheme($0)) } ) - ) { - quarterPickerSheet - } + ) + case .pushNotification: + PushNotificationSettingsView(viewModel: coordinator.makePushNotificationSettingsViewModel()) + case .account: + AccountView(viewModel: coordinator.makeAccountViewModel()) } } @@ -354,7 +375,11 @@ struct ProfileView: View { ForEach(activities) { activity in Button { if !activity.isDeleted { - coordinator.router.push(.activity(activity.todoId)) + if isCompactLayout { + coordinator.router.push(.activity(activity.todoId)) + } else { + coordinator.router.replace(with: .activity(activity.todoId)) + } } } label: { let item = TodoCategoryItem(from: activity.category)