Skip to content

Instantly share code, notes, and snippets.

@jz709u
Forked from mecid/Calendar.swift
Last active March 2, 2023 03:41
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jz709u/ed97507a8655ce5b23e205b0feea80bb to your computer and use it in GitHub Desktop.
SwiftUI Calendar view using LazyVGrid
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 CalendarView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let interval: DateInterval
let showHeaders: Bool
let content: (Date) -> DateView
init(
interval: DateInterval,
showHeaders: Bool = true,
@ViewBuilder content: @escaping (Date) -> DateView
) {
self.interval = interval
self.showHeaders = showHeaders
self.content = content
months = calendar.generateDates(
inside: interval,
matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0)
)
for month in months {
if showHeaders {
monthHeaders += [header(for: month)]
}
monthToDays[month] = days(for: month)
}
}
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(), count: 7)) {
ForEach(Array(months.enumerated()), id: \.offset) { (index,month) in
Section(header: headerView(for: monthHeaders[index])) {
if let days = monthToDays[month] {
ForEach(days, id: \.self) { date in
if calendar.isDate(date, equalTo: month, toGranularity: .month) {
content(date).id(date)
} else {
content(date).hidden()
}
}
}
}
}
}
}
private var months = [Date]()
private var monthHeaders = [String]()
private var monthToDays = [Date: [Date]]()
// Important: dont add padding or any frame modifications to the headerView or you will get performance issues
private func headerView(for month:String) -> some View {
Text(month)
.font(.title)
}
private func header(for month: Date) -> String {
let component = calendar.component(.month, from: month)
let formatter = component == 1 ? DateFormatter.monthAndYear : .month
return formatter.string(from: month)
}
private func days(for month: Date) -> [Date] {
guard
let monthInterval = calendar.dateInterval(of: .month, for: month),
let monthFirstWeek = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start),
let monthLastWeek = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.end)
else { return [] }
return calendar.generateDates(
inside: DateInterval(start: monthFirstWeek.start, end: monthLastWeek.end),
matching: DateComponents(hour: 0, minute: 0, second: 0)
)
}
}
struct CalendarView_Previews: PreviewProvider {
static var previews: some View {
CalendarView(interval: .init()) { _ in
Text("30")
.padding(8)
.background(Color.blue)
.cornerRadius(8)
}
}
}
@chrisriner
Copy link

If I use this and do an interval of a month and I do a for each and generate a years worth of months one at a time and then have them all displayed in a scroll view then I encounter an issue where as I am scrolling I get a little jump on the screen from each month so something is happening in the view or loading of the view that causes a performance problem maybe or something about how it draws etc. I am trying to generate a year calendar but do it one month at a time and allow it to be scrolled and I need to do this as I need to do things for each month and can't do that if I load a full year at once with a year interval. Any suggestion?

@jz709u
Copy link
Author

jz709u commented Feb 24, 2021

@chrisriner
sorry for the late response but can you send me the code you are using maybe I could further understand the issue you are seeing

@chrisriner
Copy link

Thanks for getting back to me but I figured out my issue.

@mecid
Copy link

mecid commented Jun 24, 2021

Hey, thanks for the improved version. SwiftUI will recreate the calendar view on every change, that's why it is better to generate dates in onAppear and store them in @State property. It should improve performance drastically.

@obskera
Copy link

obskera commented Jan 17, 2022

As a complete newbie regarding Date and Calendar related stuff, how do you use this view? When I try to use "CalendarView()" it just starts saying "Generic parameter 'DateView' could not be inferred" and I'm not exactly sure what I am supposed to be doing to actually USE the view once it's written up :/

@mecid
Copy link

mecid commented Jan 18, 2022

@obskera take a look at CalendarView_Previews struct, it shows how to use calendar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment