Skip to content

Instantly share code, notes, and snippets.

@mecid
Last active August 23, 2024 16:39
Show Gist options
  • Save mecid/f8859ea4bdbd02cf5d440d58e936faec to your computer and use it in GitHub Desktop.
Save mecid/f8859ea4bdbd02cf5d440d58e936faec to your computer and use it in GitHub Desktop.
SwiftUI Calendar view using LazyVGrid
import SwiftUI
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
}
}
extension DateFormatter {
static let monthAndYear: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("MMMM yyyy")
return formatter
}()
}
struct EquatableCalendarView<DateView: View, Value: Equatable>: View, Equatable {
static func == (
lhs: EquatableCalendarView<DateView, Value>,
rhs: EquatableCalendarView<DateView, Value>
) -> Bool {
lhs.interval == rhs.interval && lhs.value == rhs.value && lhs.showHeaders == rhs.showHeaders
}
let interval: DateInterval
let value: Value
let showHeaders: Bool
let onHeaderAppear: (Date) -> Void
let content: (Date) -> DateView
init(
interval: DateInterval,
value: Value,
showHeaders: Bool = true,
onHeaderAppear: @escaping (Date) -> Void = { _ in },
@ViewBuilder content: @escaping (Date) -> DateView
) {
self.interval = interval
self.value = value
self.showHeaders = showHeaders
self.onHeaderAppear = onHeaderAppear
self.content = content
}
var body: some View {
CalendarView(
interval: interval,
showHeaders: showHeaders,
onHeaderAppear: onHeaderAppear
) { date in
content(date)
}
}
}
struct CalendarView<DateView>: View where DateView: View {
let interval: DateInterval
let showHeaders: Bool
let onHeaderAppear: (Date) -> Void
let content: (Date) -> DateView
@Environment(\.sizeCategory) private var contentSize
@Environment(\.calendar) private var calendar
@State private var months: [Date] = []
@State private var days: [Date: [Date]] = [:]
private var columns: [GridItem] {
let spacing: CGFloat = contentSize.isAccessibilityCategory ? 2 : 8
return Array(repeating: GridItem(spacing: spacing), count: 7)
}
var body: some View {
LazyVGrid(columns: columns) {
ForEach(months, id: \.self) { month in
Section(header: header(for: month)) {
ForEach(days[month, default: []], id: \.self) { date in
if calendar.isDate(date, equalTo: month, toGranularity: .month) {
content(date).id(date)
} else {
content(date).hidden()
}
}
}
}
}
.onAppear {
months = calendar.generateDates(
inside: interval,
matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0)
)
days = months.reduce(into: [:]) { current, month in
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 }
current[month] = calendar.generateDates(
inside: DateInterval(start: monthFirstWeek.start, end: monthLastWeek.end),
matching: DateComponents(hour: 0, minute: 0, second: 0)
)
}
}
}
private func header(for month: Date) -> some View {
Group {
if showHeaders {
Text(DateFormatter.monthAndYear.string(from: month))
.font(.title)
.padding()
}
}
.onAppear { onHeaderAppear(month) }
}
}
@saul-melo
Copy link

@basememara for your month-traversing implementation is there a way to retain the selected date when switching to another month? as it stands it updates selected date to the same day in the month that is moved to.

@phuongit0301
Copy link

Hi you guys.
Do you know how I can draw a background like image below?

Calendar—Left Frame

@vinhnx
Copy link

vinhnx commented Oct 21, 2023

thank you @mecid for this gist,

I even built an Calendar app using your component

https://github.com/vinhnx/Clendar

@iletai
Copy link

iletai commented Nov 23, 2023

Thank you for sharing. It's changed my mindset about my calendar before. I had to think about the calendar, it's faster if you use the library
But with swiftui, it's more flexible. It doesn't take as much time as I thought

  • I personally think it should become a repository for more contributions, updates, and migration. Wait for it.
    @mecid

@iletai
Copy link

iletai commented Nov 30, 2023

From the idea of this gist. I had created an example for a Calendar that can be customized more. Thank you.
Hope this helps someone.
https://github.com/iletai/SwiftUICalendarView
image

@anbu-tlc
Copy link

@mecid, @filimo, @funkenstrahlen How can I enable horizontal scrolling for this custom table calendar? Currently, when I scroll horizontally, it navigates to the next or previous month. I've been searching for a solution so far. If anyone knows the answer, please share it with me. Thank you.

@AbdullohBahromjonov
Copy link

@mecid, @basememara, @iletai, @acegreen How I can make Monday to come first instead of Sunday

@iletai
Copy link

iletai commented Jul 1, 2024

@mecid, @basememara, @iletai, @acegreen How I can make Monday to come first instead of Sunday

Just change first week date of your calendar.

@mecid
Copy link
Author

mecid commented Jul 10, 2024

@AbdullohBahromjonov, it depends on your calendar preferences. So, if in your phone settings, the week starts on Monday, then it will be the same here.

@AbdullohBahromjonov
Copy link

@AbdullohBahromjonov, it depends on your calendar preferences. So, if in your phone settings, the week starts on Monday, then it will be the same here.

thank you!
But I have another issue here. I am using EnvironmentObject to make changes on the view but the calendar is not displaying changes instantly, it displays changes after I some random day on the calendar.
Can someone help me with this?

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