Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Calendar view in SwiftUI
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 WeekView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let week: Date
let content: (Date) -> DateView
init(week: Date, @ViewBuilder content: @escaping (Date) -> DateView) {
self.week = week
self.content = content
}
private var days: [Date] {
guard
let weekInterval = calendar.dateInterval(of: .weekOfYear, for: week)
else { return [] }
return calendar.generateDates(
inside: weekInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0)
)
}
var body: some View {
HStack {
ForEach(days, id: \.self) { date in
HStack {
if self.calendar.isDate(self.week, equalTo: date, toGranularity: .month) {
self.content(date)
} else {
self.content(date).hidden()
}
}
}
}
}
}
struct MonthView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let month: Date
let showHeader: Bool
let content: (Date) -> DateView
init(
month: Date,
showHeader: Bool = true,
@ViewBuilder content: @escaping (Date) -> DateView
) {
self.month = month
self.content = content
self.showHeader = showHeader
}
private var weeks: [Date] {
guard
let monthInterval = calendar.dateInterval(of: .month, for: month)
else { return [] }
return calendar.generateDates(
inside: monthInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0, weekday: calendar.firstWeekday)
)
}
private var header: some View {
let component = calendar.component(.month, from: month)
let formatter = component == 1 ? DateFormatter.monthAndYear : .month
return Text(formatter.string(from: month))
.font(.title)
.padding()
}
var body: some View {
VStack {
if showHeader {
header
}
ForEach(weeks, id: \.self) { week in
WeekView(week: week, content: self.content)
}
}
}
}
struct CalendarView<DateView>: View where DateView: View {
@Environment(\.calendar) var calendar
let interval: DateInterval
let content: (Date) -> DateView
init(interval: DateInterval, @ViewBuilder content: @escaping (Date) -> DateView) {
self.interval = interval
self.content = content
}
private var months: [Date] {
calendar.generateDates(
inside: interval,
matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0)
)
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(months, id: \.self) { month in
MonthView(month: month, content: self.content)
}
}
}
}
}
struct RootView: View {
@Environment(\.calendar) var calendar
private var year: DateInterval {
calendar.dateInterval(of: .year, for: Date())!
}
var body: some View {
CalendarView(interval: year) { date in
Text("30")
.hidden()
.padding(8)
.background(Color.blue)
.clipShape(Circle())
.padding(.vertical, 4)
.overlay(
Text(String(self.calendar.component(.day, from: date)))
)
}
}
}
@lailo

This comment has been minimized.

Copy link

lailo commented May 6, 2020

@mecid thanks for sharing this gist.

I'm asking myself why you used hidden() and overlay{} in the lines 157 - 165 instead of .frame(). Something like this:

struct RootView: View {
  @Environment(\.calendar) var calendar
  
  private var year: DateInterval {
    calendar.dateInterval(of: .year, for: Date())!
  }
  
  var body: some View {
    CalendarView(interval: year) { date in
      Text(String(self.calendar.component(.day, from: date)))
        .frame(width: 40, height: 40, alignment: .center)
        .background(Color.blue)
        .clipShape(Circle())
        .padding(.vertical, 4)
    }
  }
}
@mecid

This comment has been minimized.

Copy link
Owner Author

mecid commented May 6, 2020

Hi @lailo, I'm not sure if you read my post, it describes this technique. Frame modifier limits the space and it is not working well for Dynamic Type. Assume that the user uses extraExtraExtraLarge font size. Your frame will cut the text inside it. But I use a template view with the maximal width and hide it. The overlay will expand as much font size expand.

@lailo

This comment has been minimized.

Copy link

lailo commented May 7, 2020

I actually came across this snippet before I saw your post. Thanks for your explanation @mecid.

@funkenstrahlen

This comment has been minimized.

Copy link

funkenstrahlen commented May 8, 2020

Thanks for sharing :)

@wmhass

This comment has been minimized.

Copy link

wmhass commented May 12, 2020

I wonder what the performance/memory consumption will be here: https://gist.github.com/mecid/f8859ea4bdbd02cf5d440d58e936faec#file-calendar-swift-L141

Will SwiftUI do some kind of reuse, or it will allocate all the views in memory?

@A320Peter

This comment has been minimized.

Copy link

A320Peter commented May 17, 2020

Thanks for the idea. Would be nice to use this snippet but in many regions if the week starts on Monday the first week row is duplicated. I was trying to figure it out but no luck so far.

@mecid

This comment has been minimized.

Copy link
Owner Author

mecid commented May 17, 2020

@A320Peter I guess the problem is hidden inside generateDates function. I need to investigate more into it.

@mecid

This comment has been minimized.

Copy link
Owner Author

mecid commented May 17, 2020

@wmhass it is not using any reuse right now. But it can change anytime without our notice.

@A320Peter

This comment has been minimized.

Copy link

A320Peter commented May 28, 2020

Is there any progress with this duplicated week issue?

@mecid

This comment has been minimized.

Copy link
Owner Author

mecid commented May 28, 2020

@A320Peter I'm going to work on it during the week.

@ekoranek12

This comment has been minimized.

Copy link

ekoranek12 commented May 29, 2020

@mecid I found that replacing DateComponents(hour: 0, minute: 0, second: 0, weekday: 1) with DateComponents(hour: 0, minute: 0, second: 0, weekday: Calendar.current.firstWeekday) in months resolves the duplicate first week issue.

@mecid

This comment has been minimized.

Copy link
Owner Author

mecid commented May 30, 2020

@ekoranek12 thanks for your contribution 🙏🏻

@mecid

This comment has been minimized.

Copy link
Owner Author

mecid commented May 30, 2020

@A320Peter Let's say thanks to @ekoranek12 the issue is solved.

@A320Peter

This comment has been minimized.

Copy link

A320Peter commented May 30, 2020

Awesome, thank you! We were so close playing with the weekday parameter but did't realise the .firstWeekday availability.

@danieleprice123

This comment has been minimized.

Copy link

danieleprice123 commented May 31, 2020

This is a great code snippet and I was hoping to expand by adding a "day view." I added an .onTapGesture to the overlay in your template view but using DayView(date: date) sends 12/29/2019 regardless of the item I click on. What am I missing/not understanding?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.