import SwiftUI import SwiftData struct HomeView: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.modelContext) private var context @Environment(\.openURL) private var openURL @Query(sort: \Dish.createdAt, order: .reverse) private var dishes: [Dish] @Query private var tags: [Tag] @Query(sort: \WeekPlan.weekStartDate, order: .forward) private var weekPlans: [WeekPlan] @Query private var allSettings: [AppSettings] @StateObject private var viewModel = HomeViewModel() @State private var editingDish: Dish? @State private var showPremiumFromExport: Bool = false @State private var showMonthlyHistory: Bool = false @State private var showWeekLimitUpsell: Bool = false @State private var wasWeekComplete: Bool = false @State private var selectedEmptySlotId: UUID? @State private var showWeekPicker: Bool = false @State private var weekPickerDate: Date = Date() @State private var showCopyPreviousConfirm: Bool = false @State private var showReviewSentimentPrompt: Bool = false @State private var showReviewSupportPrompt: Bool = false @State private var showPostOnboardingAutoAssignPrompt: Bool = false @State private var showPostOnboardingPremiumPrompt: Bool = false @State private var hasEvaluatedPostOnboardingPrompts: Bool = false private var settings: AppSettings? { allSettings.first } var body: some View { NavigationStack { ZStack { Color.mealMoodBackground.ignoresSafeArea() if let settings = settings { let plan = fetchWeekPlan(for: viewModel.currentWeekStart) if let plan { mainContent(plan: plan, settings: settings) } } } .toast(isShowing: $viewModel.showToast, message: viewModel.toastMessage) .navigationTitle("") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(Color.mealMoodCoral.opacity(0.22), for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .toolbar { ToolbarItem(placement: .navigationBarLeading) { HStack(spacing: 12) { NavigationLink(destination: SettingsView()) { Image(systemName: "gearshape") .foregroundColor(.mealMoodTextPrimary) } if settings?.isPremium == true { Button { showMonthlyHistory = true } label: { Image(systemName: "calendar") .foregroundColor(.mealMoodTextPrimary) } } } } ToolbarItem(placement: .principal) { HStack(spacing: 10) { if let settings = settings { Button { viewModel.goToPreviousWeek() showWeekLimitUpsell = false } label: { Image(systemName: "chevron.left") .foregroundColor(.mealMoodCoral) } .accessibilityLabel(Text("home_previous")) Button { weekPickerDate = viewModel.currentWeekStart showWeekPicker = true } label: { VStack(spacing: 2) { Text(viewModel.weekRangeText) .font(.mealMoodH2) .foregroundColor(.mealMoodTextPrimary) } } .buttonStyle(.plain) .accessibilityLabel(Text("home_select_week")) Button { if canNavigateToNextWeek(settings: settings) { viewModel.goToNextWeek() showWeekLimitUpsell = false } else { withAnimation(.easeInOut(duration: 0.2)) { showWeekLimitUpsell = true } showPremiumFromExport = true HapticManager.shared.notification(type: .warning) } } label: { Image(systemName: "chevron.right") .foregroundColor(.mealMoodCoral) } .accessibilityLabel(Text("home_next")) } } } ToolbarItem(placement: .navigationBarTrailing) { HStack(spacing: 10) { if let settings = settings, let plan = fetchWeekPlan(for: viewModel.currentWeekStart) { if viewModel.canEditCurrentWeek { Button { viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings) } label: { Image(systemName: "wand.and.stars") .foregroundColor(.mealMoodTextPrimary) } .contextMenu { Button { viewModel.undoLastAction(plan: plan, settings: settings) } label: { Label("home_undo_last_action", systemImage: "arrow.uturn.backward") } .disabled(!viewModel.canUndo(for: plan)) Button { if plan.slots.contains(where: { $0.dishId != nil }) { showCopyPreviousConfirm = true } else { copyFromPreviousWeek(currentPlan: plan, settings: settings) } } label: { Label("home_copy_previous_week", systemImage: "doc.on.doc") } Button(role: .destructive) { viewModel.showResetAlert = true } label: { Label("home_reset", systemImage: "arrow.counterclockwise") } } .accessibilityLabel(Text("home_complete")) .disabled(viewModel.isAutoCompleting || dishes.isEmpty) if settings.syncEnabled && settings.syncModeEnum == .manual { Button { viewModel.syncWeekToCalendar(plan: plan, dishes: dishes, settings: settings) } label: { Image(systemName: "arrow.triangle.2.circlepath") .foregroundColor(.mealMoodTextPrimary) } .accessibilityLabel(Text("settings_sync_now")) } } } } } } } .onAppear { guard let settings = settings, let plan = ensureCurrentWeekPlanExists(settings: settings) else { return } viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) } .onChange(of: settings?.includeWeekends) { _, _ in guard let settings = settings, let plan = ensureCurrentWeekPlanExists(settings: settings) else { return } viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) } .onChange(of: settings?.mealWindows) { _, _ in guard let settings = settings, let plan = ensureCurrentWeekPlanExists(settings: settings) else { return } viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) } .onChange(of: viewModel.currentWeekStart) { _, _ in guard let settings = settings, let plan = ensureCurrentWeekPlanExists(settings: settings) else { return } viewModel.reconcileSlotsIfNeeded(plan: plan, settings: settings, context: context) } } @ViewBuilder private func mainContent(plan: WeekPlan, settings: AppSettings) -> some View { VStack(spacing: 0) { if showWeekLimitUpsell && !settings.isPremium { PremiumUpsellBanner( messageKey: "premium_limit_future_weeks", actionTitleKey: "premium_subscribe" ) { showPremiumFromExport = true } .padding(.horizontal, 16) .padding(.bottom, 8) } VStack(spacing: 10) { WeekCalendarView( plan: plan, settings: settings, dishes: dishes, tags: tags, viewModel: viewModel, onTapEmptySlot: { slot in selectedEmptySlotId = slot.id } ) if isWeekComplete(plan: plan) { exportCallout(plan: plan, settings: settings) } ScrollView(showsIndicators: true) { dishDrawer(plan: plan, settings: settings) .padding(.bottom, 0) } .frame(maxHeight: .infinity) } .frame(maxWidth: horizontalSizeClass == .regular ? 1160 : .infinity) .frame(maxWidth: .infinity, alignment: .top) .padding(.horizontal, horizontalSizeClass == .regular ? 14 : 0) .padding(.bottom, 0) } .frame(maxHeight: .infinity, alignment: .top) .safeAreaInset(edge: .bottom, spacing: 0) { if !settings.isPremium { AdBannerView() } } .alert("reset_title", isPresented: $viewModel.showResetAlert) { Button(String(localized: "reset_cancel"), role: .cancel) {} Button(String(localized: "reset_confirm"), role: .destructive) { viewModel.resetWeek(plan: plan, settings: settings) } } message: { Text("reset_message") } .alert("copy_previous_confirm_title", isPresented: $showCopyPreviousConfirm) { Button("reset_cancel", role: .cancel) {} Button("copy_previous_confirm_confirm", role: .destructive) { copyFromPreviousWeek(currentPlan: plan, settings: settings) } } message: { Text("copy_previous_confirm_message") } .alert(item: $viewModel.invalidDropContext) { context in Alert( title: Text("rule_override_title"), message: Text("rule_override_message"), primaryButton: .destructive(Text("rule_override_confirm")) { viewModel.assignDish(context.dish, to: context.slot, plan: plan, settings: settings, isOverride: true) }, secondaryButton: .cancel(Text("reset_cancel")) ) } .alert("onboarding_auto_assign_title", isPresented: $showPostOnboardingAutoAssignPrompt) { Button("onboarding_auto_assign_yes") { viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings) UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoAssignPromptKey) schedulePostOnboardingPremiumPromptIfNeeded(settings: settings) } Button("onboarding_auto_assign_no", role: .cancel) { UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoAssignPromptKey) schedulePostOnboardingPremiumPromptIfNeeded(settings: settings) } } message: { Text("onboarding_auto_assign_message") } .alert("onboarding_premium_prompt_title", isPresented: $showPostOnboardingPremiumPrompt) { Button("onboarding_premium_prompt_cta") { UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey) showPremiumFromExport = true } Button("reset_cancel", role: .cancel) { UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey) } } message: { Text("onboarding_premium_prompt_message") } .alert("review_funnel_title", isPresented: $showReviewSentimentPrompt) { Button("review_funnel_positive") { ReviewPromptService.shared.requestReview() ReviewPromptService.shared.markReviewCompleted() } Button("review_funnel_negative", role: .destructive) { showReviewSupportPrompt = true } Button("reset_cancel", role: .cancel) {} } message: { Text("review_funnel_message") } .alert("review_feedback_title", isPresented: $showReviewSupportPrompt) { Button("review_feedback_contact") { if let url = URL(string: "mailto:support@mealmood.app?subject=MealMood%20Feedback") { openURL(url) } } Button("reset_cancel", role: .cancel) {} } message: { Text("review_feedback_message") } .sheet(isPresented: $viewModel.showDishForm) { DishFormView() } .sheet(isPresented: $showWeekPicker) { NavigationStack { Form { DatePicker( "home_week_picker_date", selection: $weekPickerDate, displayedComponents: .date ) .datePickerStyle(.graphical) } .scrollContentBackground(.hidden) .background(Color.mealMoodBackground) .navigationTitle("home_week_picker_title") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("reset_cancel") { showWeekPicker = false } } ToolbarItem(placement: .confirmationAction) { Button("home_week_picker_go") { jumpToSelectedWeek() showWeekPicker = false } } } } .presentationDetents([.medium, .large]) } .sheet( isPresented: Binding( get: { selectedEmptySlotId != nil }, set: { isPresented in if !isPresented { selectedEmptySlotId = nil } } ) ) { if let slotId = selectedEmptySlotId, plan.slots.contains(where: { $0.id == slotId }) { SlotDishPickerSheet( dishes: dishes, tags: tags, onPickDish: { dish in guard let freshSlot = plan.slots.first(where: { $0.id == slotId }) else { selectedEmptySlotId = nil return } let isValid = AutocompleteEngine.validateDrop( dish: dish, slot: freshSlot, plan: plan, allTags: tags, allDishes: dishes ) if isValid { viewModel.assignDish(dish, to: freshSlot, plan: plan, settings: settings) } else { // In picker flow, assign anyway and mark as override so the action is never lost. viewModel.assignDish(dish, to: freshSlot, plan: plan, settings: settings, isOverride: true) } selectedEmptySlotId = nil }, onCreateDish: { selectedEmptySlotId = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { viewModel.showDishForm = true } } ) } } .sheet(item: $editingDish) { dish in DishFormView(dish: dish) } .sheet(isPresented: $showPremiumFromExport) { NavigationStack { PremiumView(settings: settings) } } .sheet(isPresented: $showMonthlyHistory) { MonthlyHistoryView( weekPlans: weekPlans, currentWeekStart: viewModel.currentWeekStart, language: settings.languageEnum.resolved() ) { weekStart in viewModel.jumpToWeek(startDate: weekStart) showMonthlyHistory = false } } .task { await NotificationService.shared.requestPermissionIfNeeded() let nextPlan = fetchWeekPlan(for: Date().startOfWeek().addingDays(7)) NotificationService.shared.schedulePlanningReminderIfNeeded( nextWeekPlan: nextPlan, language: settings.languageEnum.resolved() ) wasWeekComplete = isWeekComplete(plan: plan) evaluatePostOnboardingPromptsIfNeeded(plan: plan, settings: settings) } .onChange(of: plan.updatedAt) { _, _ in let nowComplete = isWeekComplete(plan: plan) if nowComplete && !wasWeekComplete { evaluateReviewPrompt() } wasWeekComplete = nowComplete } } private struct SlotDishPickerSheet: View { let dishes: [Dish] let tags: [Tag] let onPickDish: (Dish) -> Void let onCreateDish: () -> Void @Environment(\.dismiss) private var dismiss @State private var searchText: String = "" private var filteredDishes: [Dish] { let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return dishes.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } return dishes .filter { $0.name.localizedCaseInsensitiveContains(trimmed) } .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } var body: some View { NavigationStack { Group { if dishes.isEmpty { VStack(spacing: 16) { Text("home_pick_dish_no_dishes") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) Button { dismiss() onCreateDish() } label: { Label("home_pick_dish_add_new", systemImage: "plus") .font(.mealMoodBodyBold) .foregroundColor(.mealMoodCoral) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(24) } else { List { ForEach(filteredDishes, id: \.id) { dish in Button { onPickDish(dish) dismiss() } label: { VStack(alignment: .leading, spacing: 6) { Text(dish.name) .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) HStack(spacing: 4) { let dishTags = tags.filter { dish.tagIds.contains($0.id) } ForEach(dishTags.prefix(3), id: \.id) { tag in TagDot(color: tag.color, size: 8) } } } .padding(.vertical, 2) } .listRowBackground(Color.mealMoodSurface) } if filteredDishes.isEmpty { Text("home_pick_dish_empty") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) .listRowBackground(Color.mealMoodSurface) } } .listStyle(.plain) .scrollContentBackground(.hidden) .background(Color.mealMoodBackground) .searchable(text: $searchText, prompt: Text("home_pick_dish_search")) } } .background(Color.mealMoodBackground.ignoresSafeArea()) .environment(\.colorScheme, .light) .navigationTitle("home_pick_dish_title") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("dish_cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button { dismiss() onCreateDish() } label: { Image(systemName: "plus") } .accessibilityLabel(Text("home_pick_dish_add_new")) } } } } } private func dishDrawer(plan: WeekPlan, settings: AppSettings) -> some View { DishDrawerView( dishes: dishes, tags: tags, language: settings.languageEnum.resolved(), usedDishIds: Set(plan.slots.compactMap(\.dishId)), onAddDish: { viewModel.showDishForm = true }, onQuickAssignDish: { dish in viewModel.assignDishToFirstFreeSlot( dish, plan: plan, settings: settings, allTags: tags, allDishes: dishes ) }, onEditDish: { dish in editingDish = dish }, onDeleteDish: { dish in let isAssignedInCurrentWeek = plan.slots.contains { $0.dishId == dish.id } if isAssignedInCurrentWeek { viewModel.toastMessage = localizedString("dish_delete_blocked_message", language: settings.languageEnum.resolved()) viewModel.showToast = true return } context.delete(dish) try? context.save() viewModel.toastMessage = localizedString("toast_dish_deleted", language: settings.languageEnum.resolved()) viewModel.showToast = true }, draggedDish: $viewModel.draggedDish ) } private func canNavigateToNextWeek(settings: AppSettings) -> Bool { if settings.isPremium { return true } let maxFreeWeek = Date().startOfWeek().addingDays(7) return viewModel.currentWeekStart < maxFreeWeek } private func canNavigateToWeek(_ startDate: Date, settings: AppSettings) -> Bool { if settings.isPremium { return true } let maxFreeWeek = Date().startOfWeek().addingDays(7) return startDate <= maxFreeWeek } private func jumpToSelectedWeek() { guard let settings else { return } let selectedWeekStart = weekPickerDate.startOfWeek() if canNavigateToWeek(selectedWeekStart, settings: settings) { viewModel.jumpToWeek(startDate: selectedWeekStart) showWeekLimitUpsell = false } else { withAnimation(.easeInOut(duration: 0.2)) { showWeekLimitUpsell = true } showPremiumFromExport = true HapticManager.shared.notification(type: .warning) } } private func isWeekComplete(plan: WeekPlan) -> Bool { plan.slots.allSatisfy { $0.dishId != nil } } @ViewBuilder private func exportCallout(plan: WeekPlan, settings: AppSettings) -> some View { VStack(alignment: .leading, spacing: 10) { Text("share_week_callout_title") .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) Text("share_week_callout_subtitle") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) if settings.isPremium, let image = renderWeekShareImage(plan: plan, settings: settings), let shareURL = persistShareImage(image) { ShareLink( item: shareURL, preview: SharePreview(String(localized: "share_week_title"), image: Image(uiImage: image)) ) { Label("share_week_button", systemImage: "square.and.arrow.up") .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) .frame(maxWidth: .infinity) .padding(.vertical, 10) .background(Color.white.opacity(0.75)) .clipShape(RoundedRectangle(cornerRadius: 10)) } } else { Button { showPremiumFromExport = true } label: { Label("share_week_button", systemImage: "star.fill") .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) .frame(maxWidth: .infinity) .padding(.vertical, 10) .background(Color.white.opacity(0.75)) .clipShape(RoundedRectangle(cornerRadius: 10)) } .buttonStyle(.plain) } } .padding(12) .background( RoundedRectangle(cornerRadius: 12) .fill(Color.mealMoodMint.opacity(0.55)) ) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.mealMoodCoral.opacity(0.4), lineWidth: 1) ) .padding(.horizontal, 16) } private func copyFromPreviousWeek(currentPlan: WeekPlan, settings: AppSettings) { let previousPlan = fetchWeekPlan(for: viewModel.currentWeekStart.addingDays(-7)) viewModel.copyFromPreviousWeek( currentPlan: currentPlan, previousPlan: previousPlan, settings: settings, allTags: tags, allDishes: dishes ) } private func renderWeekShareImage(plan: WeekPlan, settings: AppSettings) -> UIImage? { let renderer = ImageRenderer(content: WeekPlanShareView(plan: plan, settings: settings, dishes: dishes, tags: tags)) renderer.proposedSize = ProposedViewSize( width: WeekPlanShareView.a4LandscapeWidth, height: WeekPlanShareView.a4LandscapeHeight ) renderer.scale = 1 return renderer.uiImage } private func persistShareImage(_ image: UIImage) -> URL? { guard let data = image.pngData() else { return nil } let url = FileManager.default.temporaryDirectory.appendingPathComponent("mealmood-week-plan.png") try? data.write(to: url, options: .atomic) return url } private func fetchWeekPlan(for weekStartDate: Date) -> WeekPlan? { let matches = weekPlanCandidates(for: weekStartDate) guard !matches.isEmpty else { return nil } return preferredWeekPlan(from: matches) } private func weekPlanCandidates(for weekStartDate: Date) -> [WeekPlan] { let descriptor = FetchDescriptor( predicate: #Predicate { plan in plan.weekStartDate == weekStartDate } ) return (try? context.fetch(descriptor)) ?? [] } private func preferredWeekPlan(from plans: [WeekPlan]) -> WeekPlan? { plans.max { lhs, rhs in let lhsAssigned = lhs.slots.filter { $0.dishId != nil }.count let rhsAssigned = rhs.slots.filter { $0.dishId != nil }.count if lhsAssigned != rhsAssigned { return lhsAssigned < rhsAssigned } if lhs.slots.count != rhs.slots.count { return lhs.slots.count < rhs.slots.count } return lhs.updatedAt < rhs.updatedAt } } private func ensureCurrentWeekPlanExists(settings: AppSettings) -> WeekPlan? { let matches = weekPlanCandidates(for: viewModel.currentWeekStart) if let preferred = preferredWeekPlan(from: matches) { if matches.count > 1 { for duplicate in matches where duplicate.id != preferred.id { context.delete(duplicate) } try? context.save() } return preferred } return DefaultDataService.createWeekPlan(for: viewModel.currentWeekStart, settings: settings, context: context) } private func evaluateReviewPrompt() { let descriptor = FetchDescriptor() guard let plans = try? context.fetch(descriptor) else { return } let completedWeeks = plans.filter { !$0.slots.isEmpty && $0.slots.allSatisfy { $0.dishId != nil } }.count guard ReviewPromptService.shared.shouldShowFunnelAfterWeekCompletion(completedWeeks: completedWeeks) else { return } ReviewPromptService.shared.markFunnelShown(completedWeeks: completedWeeks) showReviewSentimentPrompt = true } private func evaluatePostOnboardingPromptsIfNeeded(plan: WeekPlan, settings: AppSettings) { guard !hasEvaluatedPostOnboardingPrompts else { return } hasEvaluatedPostOnboardingPrompts = true let shouldAskAutoAssign = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingAutoAssignPromptKey) if shouldAskAutoAssign && plan.slots.contains(where: { $0.dishId == nil }) { showPostOnboardingAutoAssignPrompt = true return } UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoAssignPromptKey) schedulePostOnboardingPremiumPromptIfNeeded(settings: settings) } private func schedulePostOnboardingPremiumPromptIfNeeded(settings: AppSettings) { guard !settings.isPremium else { UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey) return } let shouldShowPremiumPrompt = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingPremiumPromptKey) guard shouldShowPremiumPrompt else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { showPostOnboardingPremiumPrompt = true } } } private struct MonthlyHistoryView: View { let weekPlans: [WeekPlan] let currentWeekStart: Date let language: AppLanguage let onSelectWeek: (Date) -> Void @Environment(\.dismiss) private var dismiss @State private var monthCursor: Date init( weekPlans: [WeekPlan], currentWeekStart: Date, language: AppLanguage, onSelectWeek: @escaping (Date) -> Void ) { self.weekPlans = weekPlans self.currentWeekStart = currentWeekStart self.language = language self.onSelectWeek = onSelectWeek _monthCursor = State(initialValue: currentWeekStart.startOfMonth()) } private var locale: Locale { Locale(identifier: language.localeIdentifier) } private var monthPlans: [WeekPlan] { let calendar = Calendar.current return weekPlans .filter { calendar.component(.year, from: $0.weekStartDate) == calendar.component(.year, from: monthCursor) && calendar.component(.month, from: $0.weekStartDate) == calendar.component(.month, from: monthCursor) } .sorted { $0.weekStartDate > $1.weekStartDate } } var body: some View { NavigationStack { VStack(spacing: 14) { monthHeader if monthPlans.isEmpty { Text("history_month_empty") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) .padding(.top, 24) } else { List(monthPlans, id: \.id) { plan in Button { onSelectWeek(plan.weekStartDate) dismiss() } label: { HStack { VStack(alignment: .leading, spacing: 4) { Text(weekLabel(for: plan.weekStartDate)) .font(.mealMoodBodyBold) .foregroundColor(.mealMoodTextPrimary) Text(plan.slots.allSatisfy { $0.dishId != nil } ? String(localized: "history_week_complete") : String(localized: "history_week_incomplete")) .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) } Spacer() Image(systemName: "chevron.right") .foregroundColor(.mealMoodTextSecondary) } .padding(.vertical, 4) } .listRowBackground(Color.mealMoodSurface) } .listStyle(.plain) .scrollContentBackground(.hidden) } } .padding(.horizontal, 16) .padding(.top, 10) .background(Color.mealMoodBackground.ignoresSafeArea()) .navigationTitle("history_title") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("tag_selector_done") { dismiss() } } } } } private var monthHeader: some View { HStack { Button { monthCursor = monthCursor.addingMonths(-1) } label: { Image(systemName: "chevron.left") .foregroundColor(.mealMoodCoral) } Spacer() Text(monthCursor.monthYearLabel(locale: locale)) .font(.mealMoodH3) .foregroundColor(.mealMoodTextPrimary) Spacer() Button { monthCursor = monthCursor.addingMonths(1) } label: { Image(systemName: "chevron.right") .foregroundColor(.mealMoodCoral) } } } private func weekLabel(for startDate: Date) -> String { let endDate = startDate.addingDays(6) let dayFormatter = DateFormatter() dayFormatter.locale = locale dayFormatter.setLocalizedDateFormatFromTemplate("d") let monthFormatter = DateFormatter() monthFormatter.locale = locale monthFormatter.setLocalizedDateFormatFromTemplate("MMM") return "\(dayFormatter.string(from: startDate))-\(dayFormatter.string(from: endDate)) \(monthFormatter.string(from: endDate))" } }