-
-
Save jz709u/ed97507a8655ce5b23e205b0feea80bb to your computer and use it in GitHub Desktop.
SwiftUI Calendar view using LazyVGrid
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
Thanks for getting back to me but I figured out my issue.
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.
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 :/
@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
@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