import SwiftUI struct CalendarMonthView: View { @ObservedObject var model: CalendarModel @Environment(\.controlSize) private var controlSize private var compact: Bool { #if os(macOS) controlSize == .small #else false #endif } @Environment(\.dynamicTypeSize) private var typeSize private let accent = Color(red: 0.78, green: 0.22, blue: 0.29) var body: some View { VStack(spacing: 0) { monthHeader; monthGrid } } private var monthHeader: some View { ViewThatFits(in: .horizontal) { HStack { Text(model.display(model.monthStart, template: "MMMM yyyy")).font(.title2.bold()) Spacer() monthControls } VStack(alignment: .leading) { Text(model.display(model.monthStart, template: "MMMM yyyy")).font(.title2.bold()) monthControls } }.padding(.vertical, compact ? 4 : 16) } private var monthControls: some View { HStack { Button("Previous month", systemImage: "chevron.left") { move(-1) }.labelStyle(.iconOnly).frame(minWidth: compact ? 28 : 44, minHeight: compact ? 28 : 44) Button("Today") { model.date = Date() }.frame(minHeight: compact ? 28 : 44) Button("Next month", systemImage: "chevron.right") { move(1) }.labelStyle(.iconOnly).frame(minWidth: compact ? 28 : 44, minHeight: compact ? 28 : 44) }.buttonStyle(.borderless) } private func move(_ amount: Int) { model.date = model.calendar.date(byAdding: .month, value: amount, to: model.date) ?? model.date } private var monthGrid: some View { Grid(horizontalSpacing: 4, verticalSpacing: 4) { GridRow { ForEach(Array(model.weekdayLabels.enumerated()), id: \.offset) { _, label in Text(typeSize.isAccessibilitySize ? String(label.prefix(1)) : label) .font(.caption).foregroundStyle(.secondary).accessibilityHidden(true) } } ForEach(0..<6) { week in GridRow { ForEach(Array(model.days.dropFirst(week * 7).prefix(7)), id: \.self) { day in dayButton(day) } } } }.padding(.bottom, 12) } private func dayButton(_ day: Date) -> some View { let selected = model.calendar.isDate(day, inSameDayAs: model.date) let today = model.calendar.isDate(day, inSameDayAs: Date()) let count = model.dayEvents(day).count let current = model.calendar.isDate(day, equalTo: model.monthStart, toGranularity: .month) return Button { model.date = day } label: { VStack(spacing: 3) { Text("\(model.calendar.component(.day, from: day))") .font(.body.monospacedDigit()).bold(today).lineLimit(1).minimumScaleFactor(0.7) Circle().fill(count == 0 ? .clear : selected ? .white : accent).frame(width: 4, height: 4) }.frame(maxWidth: .infinity, minHeight: compact ? 28 : 44) .foregroundStyle(selected ? .white : current ? Color.primary : Color.secondary) .background(selected ? accent : .clear, in: RoundedRectangle(cornerRadius: 9)) .overlay { RoundedRectangle(cornerRadius: 9).strokeBorder(today ? accent : .clear, lineWidth: 2) } }.buttonStyle(.plain) .accessibilityLabel(model.display(day, template: "EEEE d MMMM yyyy")) .accessibilityValue("\(today ? "Today. " : "")\(count) events") .accessibilityAddTraits(selected ? .isSelected : []) } }