diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index bf80844..bc7cf72 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -294,6 +294,8 @@ struct CalendarView: View { }() @State private var viewMode: CalendarViewMode = .month @State private var calTaskListMode: Bool = false + /// Year currently at the top of the continuous year scroll (drives the header in .year mode). + @State private var visibleYear: Int = Calendar.current.component(.year, from: Date()) @State private var showAddTask = false @State private var showCalendarConnect = false @State private var showWorkoutSession = false @@ -333,7 +335,7 @@ struct CalendarView: View { // Center: month + year Spacer() - Text(monthTitle) + Text(headerTitle) .font(AppFonts.sans(17, weight: .semibold)) .foregroundColor(AppColors.text(cs)) Spacer() @@ -434,6 +436,12 @@ struct CalendarView: View { } } + /// In the continuous year view the header follows the scrolled-to year; + /// otherwise it's the month + year of the displayed month. + private var headerTitle: String { + viewMode == .year ? String(visibleYear) : monthTitle + } + private var monthTitle: String { let f = DateFormatter(); f.dateFormat = "MMMM yyyy" return f.string(from: displayMonth) @@ -673,6 +681,15 @@ struct CalendarView: View { // MARK: - Year + // Reports each year block's top offset within the year scroll, so the + // header can follow whichever year is currently at the top. + private struct YearTopKey: PreferenceKey { + static var defaultValue: [Int: CGFloat] = [:] + static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) { + value.merge(nextValue()) { _, new in new } + } + } + // Continuous, TickTick-style: years flow one after another in an infinite // scroll. Opens anchored on the current year; scroll up for past years, // down for future ones — no paging, no dead space below December. @@ -693,7 +710,22 @@ struct CalendarView: View { .padding(.top, 8) .padding(.bottom, 96) // clear the floating tab bar / + button } - .onAppear { proxy.scrollTo(currentYear, anchor: .top) } + .coordinateSpace(name: "yearScroll") + .onPreferenceChange(YearTopKey.self) { tops in + // The header year is the topmost block that has reached the top edge: + // among blocks at/above a small threshold, the one nearest the top. + let threshold: CGFloat = 60 + let reached = tops.filter { $0.value <= threshold } + if let y = reached.max(by: { $0.value < $1.value })?.key + ?? tops.min(by: { $0.value < $1.value })?.key, + y != visibleYear { + visibleYear = y + } + } + .onAppear { + visibleYear = currentYear + proxy.scrollTo(currentYear, anchor: .top) + } } } @@ -712,6 +744,14 @@ struct CalendarView: View { } .padding(.horizontal, 16) } + .background( + GeometryReader { geo in + Color.clear.preference( + key: YearTopKey.self, + value: [year: geo.frame(in: .named("yearScroll")).minY] + ) + } + ) } private func cvMiniMonth(_ month: Date) -> some View {