I’m working on building a calendar UI in SwiftUI using LazyVGrid. My intention is for the calendar to correctly display the dates of the month, adjusting for the position of the first day of the month (for example, if a month begins on a Wednesday, then the "1" should appear under the "Wed" column).
The issue I’m facing is that when I switch to a new month by updating the currentMonth state, the LazyVGrid doesn’t seem to update the positioning of the dates correctly. While the days in the month change according to the new month, the first day’s position remains the same as the previous month.
Here’s a simplified snippet of the code I’ve been working with:
import SwiftUI
struct SimpleCalendarView: View {
@State private var currentMonth = Date()
private var daysInMonth: Int {
let range = Calendar.current.range(
of: .day,
in: .month, for: currentMonth
)
return range?.count ?? 0
}
private var firstDayOfMonth: Int {
let components = Calendar.current.dateComponents(
[.year, .month],
from: currentMonth
)
guard let firstDay = Calendar.current.date(from: components) else {
return 1 // Default
}
return Calendar.current.component(.weekday, from: firstDay)
}
private let columns: [GridItem] = Array(
repeating: .init(.flexible()), count: 7
)
var body: some View {
VStack {
HStack {
Button(action: {
self.currentMonth = Calendar.current.date(
byAdding: .month,
value: -1,
to: self.currentMonth
) ?? self.currentMonth
}, label: {
Text("Previous Month")
})
Button(action: {
self.currentMonth = Calendar.current.date(
byAdding: .month,
value: 1,
to: self.currentMonth
) ?? self.currentMonth
}, label: {
Text("Next Month")
})
}
LazyVGrid(columns: columns) {
// Add padding for the start day of the month
ForEach(1..<firstDayOfMonth) { _ in
Text("")
}
ForEach(1...daysInMonth, id: .self) { day in
Text("(day)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
.padding()
}
}
How can make the view update to reflect this change, so that the first day (1) does not always start on the same Weekday?
2
Answers
The issue seems to be related to the way
LazyVGrid
handles updates in the SwiftUI lifecycle.When navigating through months, the expected behavior is for the calendar to correctly adjust the starting position of the month's first day. If the previous month started on a Wednesday, for example, and the next month starts on a Friday, we'd expect two empty cells at the beginning of the grid for the next month. However, as observed,
LazyVGrid
does not always update to reflect these changes.To force
LazyVGrid
to redraw and respect the changes, a key can be assigned to it that changes with every update, in this case I just used thecurrentMonth
, as when I press the button this changes. This tells informs SwiftUI that this component should be redrawn from scratch.Here's how I fixed it:
Adding
.id(currentMonth)
tells SwiftUI to treat theLazyVGrid
as a new view whenever the currentMonth changes, causing it to redraw entirely and respect the changes in the layout.SwiftUI updates the views based upon their identity. By default, it will assign the identities consecutively. So, it sees that nothing has changed when you switch months because the views are still just (View(0), View(1), …).
If you give each day an identity based upon the actual date, then the views will appear in their new location. Also, give the padding ids (…, View(-3), View(-2), View(-1), View(0)).
For Fun
Now, if you surround the assignment of
self.currentMonth
withwithAnimation
, you can see the days animate to their new positions: