import SwiftUI struct MonthCalendarView: View { let habit: Habit let onToggle: (Date) -> Void @State private var monthStart = Calendar.german.startOfMonth(for: .now) private let weekdaySymbols = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] private let columns = Array(repeating: GridItem(.flexible()), count: 7) private var calendar: Calendar { .german } private var today: Date { calendar.startOfDay(for: .now) } var body: some View { VStack(spacing: 12) { header LazyVGrid(columns: columns, spacing: 8) { ForEach(weekdaySymbols, id: \.self) { symbol in Text(symbol) .font(.caption2) .foregroundStyle(.secondary) } ForEach(Array(dayCells.enumerated()), id: \.offset) { _, date in if let date { dayCell(for: date) } else { Color.clear.frame(height: 36) } } } } } private var header: some View { HStack { Button { shiftMonth(by: -1) } label: { Image(systemName: "chevron.left") } Spacer() Text(monthTitle) .font(.headline) Spacer() Button { shiftMonth(by: 1) } label: { Image(systemName: "chevron.right") } .disabled(isCurrentMonthOrLater) } .buttonStyle(.borderless) } private func dayCell(for date: Date) -> some View { let isDone = habit.isCompleted(on: date, calendar: calendar) let isToday = calendar.isDate(date, inSameDayAs: today) let isFuture = date > today let color = Color(hex: habit.colorHex) return Text("\(calendar.component(.day, from: date))") .font(.callout) .frame(width: 36, height: 36) .background { if isDone { Circle().fill(color) } else if isToday { Circle().strokeBorder(color, lineWidth: 1.5) } } .foregroundStyle(isDone ? .white : (isFuture ? Color.secondary.opacity(0.4) : .primary)) .contentShape(Circle()) .onTapGesture { if !isFuture { onToggle(date) } } } private var monthTitle: String { let formatter = DateFormatter() formatter.calendar = calendar formatter.locale = Locale(identifier: "de_DE") formatter.dateFormat = "LLLL yyyy" return formatter.string(from: monthStart) } /// Leading nil padding for the weekday offset, then one date per day in the month. private var dayCells: [Date?] { guard let range = calendar.range(of: .day, in: .month, for: monthStart) else { return [] } let weekday = calendar.component(.weekday, from: monthStart) let leadingBlanks = (weekday - calendar.firstWeekday + 7) % 7 var cells: [Date?] = Array(repeating: nil, count: leadingBlanks) for day in range { cells.append(calendar.date(byAdding: .day, value: day - 1, to: monthStart)) } return cells } private var isCurrentMonthOrLater: Bool { monthStart >= calendar.startOfMonth(for: .now) } private func shiftMonth(by value: Int) { if let next = calendar.date(byAdding: .month, value: value, to: monthStart) { monthStart = next } } } extension Calendar { /// Gregorian calendar with Monday as the first weekday (German convention). static var german: Calendar { var calendar = Calendar(identifier: .gregorian) calendar.firstWeekday = 2 return calendar } func startOfMonth(for date: Date) -> Date { let components = dateComponents([.year, .month], from: date) return self.date(from: components) ?? startOfDay(for: date) } }