Skip to content

Instantly share code, notes, and snippets.

@huynguyencong
Created October 2, 2020 14:50
Show Gist options
  • Save huynguyencong/f0423b1068d0edeef948a5b6b1c66967 to your computer and use it in GitHub Desktop.
Save huynguyencong/f0423b1068d0edeef948a5b6b1c66967 to your computer and use it in GitHub Desktop.
//
// CalendarView.swift
// iWidget
//
// Created by Huy Nguyen on 10/2/20.
//
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 {
let week: Date
let calendar: Calendar
let highlightDate: Date?
let content: (Date, Bool) -> DateView
init(week: Date, calendar: Calendar, highlightDate: Date?, @ViewBuilder content: @escaping (Date, Bool) -> DateView) {
self.week = week
self.calendar = calendar
self.highlightDate = highlightDate
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(spacing: 0) {
ForEach(days, id: \.self) { date in
if calendar.isDate(self.week, equalTo: date, toGranularity: .month) {
content(date, isHighlightDate(date))
} else {
content(date, false)
.hidden()
}
}
}
}
func isHighlightDate(_ date: Date) -> Bool {
guard let highlightDate = highlightDate else { return false }
return calendar.isDate(date, inSameDayAs: highlightDate)
}
}
struct MonthView<DateView: View, WeekDate: View>: View {
let month: Date
let calendar: Calendar
let weekDateView: (String) -> WeekDate
let content: (Date, Bool) -> DateView
let highlightDate: Date?
init(month: Date, calendar: Calendar, highlightDate: Date?, @ViewBuilder weekDateView: @escaping (String) -> WeekDate, @ViewBuilder content: @escaping (Date, Bool) -> DateView) {
self.month = month
self.calendar = calendar
self.highlightDate = highlightDate
self.weekDateView = weekDateView
self.content = content
}
init(month: Int, year: Int, calendar: Calendar, highlightDate: Date? = nil, @ViewBuilder weekDateView: @escaping (String) -> WeekDate, @ViewBuilder content: @escaping (Date, Bool) -> DateView) {
var dateComponents = DateComponents()
dateComponents.setValue(month, for: .month)
dateComponents.setValue(year, for: .year)
let month = Calendar.current.date(from: dateComponents) ?? Date()
self.init(month: month, calendar: calendar, highlightDate: highlightDate, weekDateView: weekDateView, content: content)
}
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 firstWeekDays: [Date] {
guard let firstWeek = weeks.first,
let weekInterval = calendar.dateInterval(of: .weekOfYear, for: firstWeek)
else { return [] }
return calendar.generateDates(
inside: weekInterval,
matching: DateComponents(hour: 0, minute: 0, second: 0)
)
}
private let dateFormater = { () -> DateFormatter in
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E"
return dateFormatter
}()
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
ForEach(firstWeekDays, id: \.self) { date in
weekDateView(dateFormater.string(from: date))
}
}
ForEach(weeks, id: \.self) { week in
WeekView(week: week, calendar: calendar, highlightDate: highlightDate, content: self.content)
}
}
}
}
struct CalendarView<DateView: View, WeekDate: View>: View {
let interval: DateInterval
let calendar: Calendar
let highlightDate: Date?
let weekDateView: (String) -> WeekDate
let content: (Date, Bool) -> DateView
init(interval: DateInterval, calendar: Calendar, highlightDate: Date?, @ViewBuilder weekDateView: @escaping (String) -> WeekDate, @ViewBuilder content: @escaping (Date, Bool) -> DateView) {
self.interval = interval
self.calendar = calendar
self.highlightDate = highlightDate
self.weekDateView = weekDateView
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, calendar: calendar, highlightDate: highlightDate, weekDateView: weekDateView, content: self.content)
}
}
}
}
}
// Usage:
struct ContentView: View {
let calendar: Calendar = { () -> Calendar in
var calendar = Calendar.current
calendar.firstWeekday = 0
return calendar
}()
var body: some View {
NavigationView {
MonthView(month: 10, year: 2020, calendar: Calendar.current, highlightDate: Date(), weekDateView: { text in
Text(text)
.font(.system(size: 13))
.frame(width: 30, height: 30)
.padding(.horizontal, 3)
}) { date, isHighlight in
Text(String(Calendar.current.component(.day, from: date)))
.foregroundColor(isHighlight ? Color.white : Color.orange)
.frame(width: 30, height: 30)
.background(isHighlight ? Color.orange : Color.clear)
.clipShape(Circle())
.padding(3)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment