Im trying to get the current month when the calendar is being scrolled. My goal is to have the header with the current month, then when the view is scrolled down to a new month the header updates with the corresponding month. I tried using a LazyVStack and in the onAppear method set the current month, but using the lazyVStack is never consistent and is always off by a few months. The code down below is optimized for an iPad calendar month view.
import SwiftUI
fileprivate extension DateFormatter {
static var month: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM"
return formatter
}
static var monthAndYear: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
return formatter
}
}
fileprivate extension Calendar {
func generateDates(
inside interval: DateInterval,
matching components: DateComponents
) -> [Date] {
var dates: [Date] = []
dates.append(interval.start)
enumerateDates(
startingAfter: interval.start,
matching: components,
matchingPolicy: .nextTime
) { date, _, stop in
if let date = date {
if date < interval.end {
dates.append(date)
} else {
stop = true
}
}
}
return dates
}
}
struct WeekView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let week: Date
let content: (Date) -> DateView
init(week: Date, @ViewBuilder content: @escaping (Date) -> DateView) {
self.week = week
self.content = content
}
private var days: [Date] {
guard
let weekInterval = calendar.dateInterval(of: .weekOfYear, for: week)
else { return [] }
return calendar.generateDates(
inside: weekInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0)
)
}
func dateInWeekened(date: Date) -> Bool {
var bool = false
if Calendar.current.isDateInWeekend(date) {
bool = true
}
return bool
}
var body: some View {
HStack(spacing: 0.0) {
ForEach(days, id: \.self) { date in
HStack(alignment: .top, spacing: 0.0) {
Divider()
VStack(alignment: .trailing, spacing: 0.0){
if self.calendar.isDate(self.week, equalTo: date, toGranularity: .month) {
HStack{
Spacer()
self.content(date)
.foregroundColor(dateInWeekened(date: date) == true ? .secondary : .primary)
.padding(8)
}
} else {
HStack{
Spacer()
self.content(date)
.hidden()
}
}
Spacer()
}
}
.frame(height: UIScreen.screenHeight / 7)
.background(dateInWeekened(date: date) == true ? Color("CalendarWeekenedColor") : Color(.systemBackground))
.edgesIgnoringSafeArea(.all)
}
}
}
}
struct MonthView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let month: Date
let showHeader: Bool
let content: (Date) -> DateView
init(
month: Date,
showHeader: Bool = true,
@ViewBuilder content: @escaping (Date) -> DateView
) {
self.month = month
self.content = content
self.showHeader = showHeader
}
private var weeks: [Date] {
guard
let monthInterval = calendar.dateInterval(of: .month, for: month)
else { return [] }
return calendar.generateDates(
inside: monthInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0, weekday: calendar.firstWeekday)
)
}
private var header: some View {
let component = calendar.component(.month, from: month)
let formatter = component == 1 ? DateFormatter.monthAndYear : .month
return
HStack{Text(formatter.string(from: month))
.font(.title)
.padding()
Spacer()
}.padding(.leading)
}
var body: some View {
VStack(spacing: 0.0) {
if showHeader {
header
}
else {
ForEach(weeks, id: \.self) { week in
WeekView(week: week, content: self.content)
.edgesIgnoringSafeArea(.all)
Divider()
.edgesIgnoringSafeArea(.all)
}
}
}
}
}
struct CalendarView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let interval: DateInterval
let content: (Date) -> DateView
@Binding var curentDate: Date
@State var position = 0
init(interval: DateInterval, @ViewBuilder content: @escaping (Date) -> DateView, currentDate: Binding<Date>) {
self.interval = interval
self.content = content
self._curentDate = currentDate
}
private var months: [Date] {
calendar.generateDates(
inside: interval,
matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0)
)
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0.0){
ForEach(months, id: \.self) { month in
MonthView(month: month, showHeader: false, content: self.content)
.edgesIgnoringSafeArea(.all)
.frame(maxHeight: .infinity)
.onAppear {
let component = calendar.component(.month, from: month)
let formatter = component == 1 ? DateFormatter.monthAndYear : .month
print("Current Month is \(formatter.string(from: month))")
}
}
}
}
}
}
struct CalendarMonthView: View {
@Environment(\.calendar) var calendar
@Binding var currentDate: Date
private var year: DateInterval {
calendar.dateInterval(of: .year, for: Date())!
}
var body: some View {
CalendarView(interval: year, content: { date in
Text("30")
.hidden()
.overlay(
Text(String(self.calendar.component(.day, from: date)))
)
}, currentDate: $currentDate)
.background(Color("NavigationBarColor"))
.edgesIgnoringSafeArea(.all)
}
}