Skip to content

Instantly share code, notes, and snippets.

@kovs705
Forked from mecid/Calendar.swift
Last active February 9, 2024 05:51
Show Gist options
  • Save kovs705/f1327fc03b82412a70fe47730c7d67ba to your computer and use it in GitHub Desktop.
Save kovs705/f1327fc03b82412a70fe47730c7d67ba to your computer and use it in GitHub Desktop.
SwiftUI Calendar view using LazyVGrid
import SwiftUI
// MARK: - CustomCalendar
struct CustomCalendar: View {
var calendar: Calendar
let yearFormatter: DateFormatter
let monthFormatter: DateFormatter
let dayFormatter: DateFormatter
let weekDayFormatter: DateFormatter
let fullFormatter: DateFormatter
@State var selectedDate = Self.now
static var now = Date() // Cache now
@State var dates: [Date] = []
init(calendar: Calendar) {
self.calendar = calendar
self.calendar.timeZone = TimeZone.autoupdatingCurrent
// LLLL для именительного падежа
self.yearFormatter = DateFormatter(dateFormat: "YYYY", calendar: calendar)
self.monthFormatter = DateFormatter(dateFormat: "LLLL", calendar: calendar)
self.dayFormatter = DateFormatter(dateFormat: "d", calendar: calendar)
self.weekDayFormatter = DateFormatter(dateFormat: "EEEEE", calendar: calendar)
self.fullFormatter = DateFormatter(dateFormat: "MMMM dd, yyyy", calendar: calendar)
dates = []
}
var body: some View {
VStack {
// Text("Selected date: \(fullFormatter.string(from: selectedDate))")
// .bold()
// .minimumScaleFactor(0.5)
// .lineLimit(1)
// .foregroundColor(.red)
// MARK: - Calendar
CalendarView(
calendar: calendar,
date: $selectedDate,
content: { date in
Button(action: {
selectedDate = date
manageDates(with: date)
}) {
ZStack {
Rectangle()
.fill(.clear)
.background(
calendar.isDate(date, inSameDayAs: CustomCalendar.now) ? Color.lkPurple.opacity(0.3) : .clear
)
.background(
Circle()
.fill(
calendar.isChosed(date: date, of: dates) ? Color.lkPurple : .clear
)
.frame(width: 62, height: 62)
)
Text(dayFormatter.string(from: date))
.foregroundStyle(calendar.isChosed(date: date, of: dates) ? Color.white : Color(uiColor: UIColor.label).opacity(0.9))
.minimumScaleFactor(0.5)
.lineLimit(1)
}
.ignoresSafeArea()
.background(
calendar.isInInterval(date, of: dates.first, and: dates.last) ? Color.lkPurple.opacity(0.1) : .clear
)
}
},
trailing: { date in
Text(dayFormatter.string(from: date))
.foregroundColor(.secondary)
.minimumScaleFactor(0.5)
.lineLimit(1)
},
header: { date in
Text(weekDayFormatter.string(from: date))
.minimumScaleFactor(0.5)
.lineLimit(1)
},
title: { date in
HStack {
// MARK: - Назад
Button {
withAnimation(.spring) {
guard let newDate = calendar.date(
byAdding: .month,
value: -1,
to: selectedDate
) else {
return
}
selectedDate = newDate
}
} label: {
Label(
title: { Text("Previous") },
icon: { Image(systemName: "chevron.left").font(.system(size: 18, weight: .bold)).foregroundStyle(Color.lkGray) }
)
.labelStyle(IconOnlyLabelStyle())
.padding(.horizontal)
.frame(maxHeight: .infinity)
}
Spacer()
// MARK: - Месяц, год
Text("\(monthFormatter.string(from: date).capitalized) \(yearFormatter.string(from: date))")
.fontWeight(.medium)
.minimumScaleFactor(0.5)
.lineLimit(1)
.foregroundStyle(Color.blue)
.padding()
Spacer()
// MARK: - Вперед
Button {
withAnimation(.spring) {
guard let newDate = calendar.date(
byAdding: .month,
value: 1,
to: selectedDate
) else {
return
}
selectedDate = newDate
}
} label: {
Label(
title: { Text("Next") },
icon: { Image(systemName: "chevron.right").font(.system(size: 18, weight: .bold)).foregroundStyle(Color.lkGray) }
)
.labelStyle(IconOnlyLabelStyle())
.padding(.horizontal)
.frame(maxHeight: .infinity)
}
}
.padding(.bottom, 6)
}
)
.equatable()
}
.padding()
}
// MARK: - DateManager
func manageDates(with selectedDate: Date) {
if dates.count == 2 {
dates.removeAll()
}
if dates.contains(selectedDate) {
dates.removeAll()
return
}
dates.append(selectedDate)
if dates.count == 2 {
for date in dates {
print(date.description(with: .current))
}
}
}
}
// MARK: - Previews
#if DEBUG
struct CalendarView_Previews: PreviewProvider {
static var previews: some View {
Group {
CustomCalendar(calendar: Calendar.current)
}
}
}
#endif
// MARK: - Calendar Helpers
extension CalendarView {
func makeDays() -> [Date] {
guard let monthInterval = calendar.dateInterval(of: .month, for: date),
let monthFirstWeek = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start),
let monthLastWeek = calendar.dateInterval(of: .weekOfMonth, for: monthInterval.end - 1)
else {
return []
}
let dateInterval = DateInterval(start: monthFirstWeek.start, end: monthLastWeek.end)
return calendar.generateDays(for: dateInterval)
}
}
// MARK: - Calendar extension
extension Calendar {
func isChosed(date: Date, of dates: Binding<[Date]>) -> Bool {
if dates.wrappedValue.contains(date) {
return true
} else {
return false
}
}
// MARK: - is in interval
func isInInterval(_ date: Date, of startDate: Date?, and endDate: Date?) -> Bool {
guard let startDate = startDate, let endDate = endDate else { return false }
var firstValue = startDate
var secondValue = endDate
if endDate < startDate {
firstValue = endDate
secondValue = startDate
}
let dateInterval = DateInterval(start: firstValue, end: secondValue)
if dateInterval.contains(date) {
return true
}
return false
}
func cornersReturner(_ date: Date, start: Date?, end: Date?) -> UIRectCorner {
guard let startDate = start, let endDate = end else { return [] }
var firstValue = startDate
var secondValue = endDate
if endDate < startDate {
firstValue = endDate
secondValue = startDate
}
if date == firstValue {
return [.topLeft, .bottomLeft]
} else if date == secondValue {
return [.topRight, .bottomRight]
} else {
return []
}
}
// MARK: - Days generator
func generateDates(
for dateInterval: DateInterval,
matching components: DateComponents
) -> [Date] {
var dates = [dateInterval.start]
enumerateDates(
startingAfter: dateInterval.start,
matching: components,
matchingPolicy: .nextTime
) { date, _, stop in
guard let date = date else { return }
guard date < dateInterval.end else {
stop = true
return
}
dates.append(date)
}
return dates
}
func generateDays(for dateInterval: DateInterval) -> [Date] {
generateDates(
for: dateInterval,
matching: dateComponents([.hour, .minute, .second], from: dateInterval.start)
)
}
}
// MARK: - Conformances
extension CalendarView: Equatable {
public static func == (lhs: CalendarView<Day, Header, Title, Trailing>, rhs: CalendarView<Day, Header, Title, Trailing>) -> Bool {
lhs.calendar == rhs.calendar && lhs.date == rhs.date
}
}
// MARK: - CalendarView
struct CalendarView<Day: View, Title: View, Trailing: View>: View {
// Injected dependencies
var calendar: Calendar = Calendar.current
@Binding var date: Date
let content: (Date) -> Day
let trailing: (Date) -> Trailing
let title: (Date) -> Title
// Constants
let daysInWeek = 7
let daysOfWeek: [Kotlinx_datetimeDayOfWeek] = [.monday, .tuesday, .wednesday, .thursday, .friday, .saturday, .sunday]
let minYear: Int
let maxYear: Int
@Binding var showYears: Bool
var years: [String] = []
// MARK: - init
public init(
minYear: Int,
maxYear: Int,
calendar: Calendar = Calendar.current,
date: Binding<Date>,
@ViewBuilder content: @escaping (Date) -> Day,
@ViewBuilder trailing: @escaping (Date) -> Trailing,
@ViewBuilder title: @escaping (Date) -> Title,
showYears: Binding<Bool>
) {
self.minYear = minYear
self.maxYear = maxYear
self.calendar = calendar
self.calendar.timeZone = TimeZone.autoupdatingCurrent
self._date = date
self.content = content
self.trailing = trailing
self.title = title
self._showYears = showYears
self.years = (minYear...maxYear).map { String($0) }
}
// MARK: - Body
public var body: some View {
let month = date.startOfMonth(using: calendar)
let days = makeDays()
return LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 40, maximum: .infinity), spacing: 0, alignment: .center), count: daysInWeek)) {
Section(header: title(month)) {
ForEach(daysOfWeek, id: \.self) { day in
Text(DateDay().localizedWeek(day: day, isShort: true))
.minimumScaleFactor(0.5)
.lineLimit(1)
.foregroundStyle(.secondary)
}
ForEach(days, id: \.self) { date in
if calendar.isDate(date, equalTo: month, toGranularity: .month) {
content(date)
.frame(minHeight: 40)
} else {
trailing(date)
.frame(minHeight: 40)
}
}
}
}
}
}
// MARK: - Date extension
extension Date {
func startOfMonth(using calendar: Calendar) -> Date {
calendar.date(
from: calendar.dateComponents([.year, .month], from: self)
) ?? self
}
}
// MARK: - DateFormatter extension
extension DateFormatter {
convenience init(dateFormat: String, calendar: Calendar) {
self.init()
self.dateFormat = dateFormat
self.calendar = calendar
}
}
@kovs705
Copy link
Author

kovs705 commented Sep 1, 2023

This code isn't completed yet, but you can already try the concept.
This is a calendar with ability to choose two dates to create the time interval between them and show it

@kovs705
Copy link
Author

kovs705 commented Sep 11, 2023

2023 September 11th, I updated this code to look better with large fonts + now you can choose 2 dates or take only one

@kovs705
Copy link
Author

kovs705 commented Feb 9, 2024

2024 February 9th, I cut the code into a few files - just like I have

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