parent
74691184dc
commit
b8293b3674
@ -0,0 +1,132 @@ |
||||
// |
||||
// CalendarView.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 12/04/2023. |
||||
// |
||||
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 CalendarView<DateView>: View where DateView: View { |
||||
|
||||
let calendar: Calendar = Calendar.current |
||||
|
||||
let interval: DateInterval |
||||
let showHeaders: Bool |
||||
|
||||
let content: (Date) -> DateView |
||||
|
||||
init( |
||||
interval: DateInterval, |
||||
showHeaders: Bool = true, |
||||
@ViewBuilder content: @escaping (Date) -> DateView |
||||
) { |
||||
self.interval = interval |
||||
self.showHeaders = showHeaders |
||||
self.content = content |
||||
|
||||
self.months = calendar.generateDates( |
||||
inside: interval, |
||||
matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0) |
||||
) |
||||
|
||||
for month in self.months { |
||||
if showHeaders { |
||||
self.monthHeaders += [month.formattedMonth] |
||||
} |
||||
self.monthToDays[month] = self.days(for: month) |
||||
} |
||||
} |
||||
|
||||
var body: some View { |
||||
LazyVGrid(columns: Array(repeating: GridItem(), count: 7)) { |
||||
ForEach(Array(months.enumerated().reversed()), id: \.offset) { (index,month) in |
||||
Section(header: self.headerView(for: monthHeaders[index])) { |
||||
if let days = self.monthToDays[month] { |
||||
ForEach(days, id: \.self) { date in |
||||
if calendar.isDate(date, equalTo: month, toGranularity: .month) { |
||||
content(date).id(date) |
||||
} else { |
||||
content(date).hidden() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private var months = [Date]() |
||||
private var monthHeaders = [String]() |
||||
private var monthToDays = [Date: [Date]]() |
||||
|
||||
// Important: dont add padding or any frame modifications to the headerView or you will get performance issues |
||||
private func headerView(for month:String) -> some View { |
||||
Text(month).font(.title) |
||||
} |
||||
|
||||
private func header(for month: Date) -> String { |
||||
let component = calendar.component(.month, from: month) |
||||
let formatter = component == 1 ? DateFormatter.monthAndYear : .month |
||||
return formatter.string(from: month) |
||||
} |
||||
|
||||
private func days(for month: Date) -> [Date] { |
||||
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 [] } |
||||
return calendar.generateDates( |
||||
inside: DateInterval(start: monthFirstWeek.start, end: monthLastWeek.end), |
||||
matching: DateComponents(hour: 0, minute: 0, second: 0) |
||||
) |
||||
} |
||||
} |
||||
|
||||
struct CalendarView_Previews: PreviewProvider { |
||||
static var previews: some View { |
||||
CalendarView(interval: DateInterval(start: Date(timeIntervalSinceNow: -2 * 31 * 24 * 3600), end: Date())) { date in |
||||
Text(date.get(.day).formatted()) |
||||
.padding(8) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,111 @@ |
||||
// |
||||
// TestActivityView.swift |
||||
// LeCountdown |
||||
// |
||||
// Created by Laurent Morvillier on 11/04/2023. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct MonthView: View { |
||||
|
||||
var date: Date |
||||
|
||||
fileprivate var days: [String] = Calendar.current.shortWeekdaySymbols |
||||
|
||||
init(date: Date) { |
||||
self.date = date |
||||
|
||||
// put Sunday at the end |
||||
self.days.append(self.days.removeFirst()) |
||||
} |
||||
|
||||
var body: some View { |
||||
|
||||
Text(self.date.formattedMonth).font(.title) |
||||
|
||||
HStack { |
||||
ForEach(0..<7) { day in |
||||
Text(self.days[day]).frame(maxWidth: .infinity) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
struct ActivityCalendarView: View { |
||||
|
||||
@Environment(\.calendar) var calendar |
||||
|
||||
var activity: Activity |
||||
|
||||
@State var monthlyDates: [Date] = [] |
||||
|
||||
@State var oldestDate: Date? = nil |
||||
|
||||
fileprivate let today = Date() |
||||
|
||||
@Environment(\.managedObjectContext) private var viewContext |
||||
|
||||
@State var recordsDate: [Date] = [] |
||||
|
||||
var body: some View { |
||||
|
||||
ScrollView { |
||||
if let oldestDate { |
||||
CalendarView(interval: DateInterval(start: oldestDate, end: today)) { date in |
||||
|
||||
ZStack { |
||||
|
||||
if self._hasRecord(date: date) { |
||||
Circle().fill(Color.green).brightness(0.5) |
||||
} |
||||
Text(date.get(.day).formatted()) |
||||
}.padding(6.0) |
||||
|
||||
} |
||||
} |
||||
} |
||||
.navigationTitle(activity.name ?? "Activity") |
||||
.onAppear { |
||||
self._load() |
||||
} |
||||
|
||||
} |
||||
|
||||
fileprivate func _load() { |
||||
|
||||
self.oldestDate = CoreDataRequests.oldestDate(context: self.viewContext, activity: self.activity) |
||||
|
||||
let predicate = NSPredicate(format: "activity = %@", self.activity) |
||||
let records: [Record] = self.viewContext.fetch(entityName: Record.entityName, predicate: predicate) |
||||
self.recordsDate = records.compactMap { $0.start } |
||||
|
||||
} |
||||
|
||||
fileprivate func _hasRecord(date: Date) -> Bool { |
||||
self.recordsDate.contains { rd in |
||||
self.calendar.isDate(rd, inSameDayAs: date) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
struct ActivityCalendarView_Previews: PreviewProvider { |
||||
|
||||
static var activity: Activity { |
||||
let context = PersistenceController.preview.container.viewContext |
||||
do { |
||||
return try context.fetch(Activity.fetchRequest()).first ?? Activity.fake(context: context) |
||||
} catch { |
||||
return Activity.fake(context: context) |
||||
} |
||||
} |
||||
|
||||
static var previews: some View { |
||||
ActivityCalendarView(activity: self.activity) |
||||
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue