-
-
Save jz709u/ed97507a8655ce5b23e205b0feea80bb to your computer and use it in GitHub Desktop.
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
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
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.
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?