Skip to content

Commit

Permalink
fix: Support injecting demo data to make it easy to generate screensh…
Browse files Browse the repository at this point in the history
…ots (#131)
  • Loading branch information
jbmorley authored Jan 24, 2024
1 parent a7b5f3c commit 4055299
Show file tree
Hide file tree
Showing 18 changed files with 496 additions and 116 deletions.
40 changes: 32 additions & 8 deletions macos/Overview.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
/* Begin PBXBuildFile section */
D81507DF25FD0D5300290DD2 /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81507DE25FD0D5300290DD2 /* Calendar.swift */; };
D81507E725FD8AF600290DD2 /* ApplicationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81507E625FD8AF600290DD2 /* ApplicationModel.swift */; };
D81507EC25FDB05100290DD2 /* EKEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81507EB25FDB05100290DD2 /* EKEvent.swift */; };
D817E2842B609C6D00346C3C /* Weekday.swift in Sources */ = {isa = PBXBuildFile; fileRef = D817E2832B609C6D00346C3C /* Weekday.swift */; };
D817E2862B60A6CD00346C3C /* MonthlySummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D817E2852B60A6CD00346C3C /* MonthlySummary.swift */; };
D832648528E8EAA300D1C1B7 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = D832648428E8EAA300D1C1B7 /* Date.swift */; };
D832648828E8EBF300D1C1B7 /* DateRangeIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D832648728E8EBF300D1C1B7 /* DateRangeIterator.swift */; };
D8805D9626039D4D00C23C11 /* Int.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8805D9526039D4D00C23C11 /* Int.swift */; };
Expand All @@ -18,6 +19,11 @@
D8A3D3D42911F105002AFC32 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A3D3D32911F105002AFC32 /* Set.swift */; };
D8B435A029119A5D008F4F6F /* WindowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B4359F29119A5D008F4F6F /* WindowModel.swift */; };
D8B435A229119A96008F4F6F /* CalendarError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B435A129119A96008F4F6F /* CalendarError.swift */; };
D8BAC11C2B5F0A1800D6A98A /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAC11B2B5F0A1800D6A98A /* SettingsView.swift */; };
D8BAC11E2B5F451C00D6A98A /* CalendarInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAC11D2B5F451C00D6A98A /* CalendarInstance.swift */; };
D8BAC1202B5F528300D6A98A /* CalendarStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAC11F2B5F528300D6A98A /* CalendarStore.swift */; };
D8BAC1222B5F531D00D6A98A /* DemoStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAC1212B5F531D00D6A98A /* DemoStore.swift */; };
D8BAC1242B5F60EE00D6A98A /* SimilarEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BAC1232B5F60EE00D6A98A /* SimilarEvents.swift */; };
D8C0ADAC2B5C3FDA00E77BDC /* material-icons-license in Resources */ = {isa = PBXBuildFile; fileRef = D8C0ADAB2B5C3FDA00E77BDC /* material-icons-license */; };
D8C296AB2B5777FA00286301 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C296AA2B5777FA00286301 /* URL.swift */; };
D8D3B3D628E7CB8700A610D4 /* Diligence in Frameworks */ = {isa = PBXBuildFile; productRef = D8D3B3D528E7CB8700A610D4 /* Diligence */; };
Expand All @@ -35,7 +41,7 @@
D8FE3AD42911700A00C6F7FE /* Interact in Frameworks */ = {isa = PBXBuildFile; productRef = D8FE3AD32911700A00C6F7FE /* Interact */; };
D8FE3AD92911789500C6F7FE /* Summary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FE3AD82911789500C6F7FE /* Summary.swift */; };
D8FE3ADB291178BF00C6F7FE /* CalendarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FE3ADA291178BF00C6F7FE /* CalendarItem.swift */; };
D8FE3ADD29117B8000C6F7FE /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FE3ADC29117B8000C6F7FE /* Event.swift */; };
D8FE3ADD29117B8000C6F7FE /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FE3ADC29117B8000C6F7FE /* CalendarEvent.swift */; };
D8FE3AE02911817400C6F7FE /* CalendarList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FE3ADF2911817400C6F7FE /* CalendarList.swift */; };
/* End PBXBuildFile section */

Expand All @@ -59,7 +65,8 @@
/* Begin PBXFileReference section */
D81507DE25FD0D5300290DD2 /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = "<group>"; };
D81507E625FD8AF600290DD2 /* ApplicationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationModel.swift; sourceTree = "<group>"; };
D81507EB25FDB05100290DD2 /* EKEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EKEvent.swift; sourceTree = "<group>"; };
D817E2832B609C6D00346C3C /* Weekday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weekday.swift; sourceTree = "<group>"; };
D817E2852B60A6CD00346C3C /* MonthlySummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthlySummary.swift; sourceTree = "<group>"; };
D832648428E8EAA300D1C1B7 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
D832648728E8EBF300D1C1B7 /* DateRangeIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateRangeIterator.swift; sourceTree = "<group>"; };
D8805D9526039D4D00C23C11 /* Int.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int.swift; sourceTree = "<group>"; };
Expand All @@ -68,6 +75,11 @@
D8A3D3D32911F105002AFC32 /* Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Set.swift; sourceTree = "<group>"; };
D8B4359F29119A5D008F4F6F /* WindowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowModel.swift; sourceTree = "<group>"; };
D8B435A129119A96008F4F6F /* CalendarError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarError.swift; sourceTree = "<group>"; };
D8BAC11B2B5F0A1800D6A98A /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
D8BAC11D2B5F451C00D6A98A /* CalendarInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarInstance.swift; sourceTree = "<group>"; };
D8BAC11F2B5F528300D6A98A /* CalendarStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarStore.swift; sourceTree = "<group>"; };
D8BAC1212B5F531D00D6A98A /* DemoStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoStore.swift; sourceTree = "<group>"; };
D8BAC1232B5F60EE00D6A98A /* SimilarEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarEvents.swift; sourceTree = "<group>"; };
D8C0ADAB2B5C3FDA00E77BDC /* material-icons-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "material-icons-license"; sourceTree = "<group>"; };
D8C296AA2B5777FA00286301 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
D8D3B3D328E7CAB000A610D4 /* diligence */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = diligence; path = ../diligence; sourceTree = "<group>"; };
Expand All @@ -93,7 +105,7 @@
D8FE3AD229116FE900C6F7FE /* interact */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = interact; path = ../interact; sourceTree = "<group>"; };
D8FE3AD82911789500C6F7FE /* Summary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Summary.swift; sourceTree = "<group>"; };
D8FE3ADA291178BF00C6F7FE /* CalendarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarItem.swift; sourceTree = "<group>"; };
D8FE3ADC29117B8000C6F7FE /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = "<group>"; };
D8FE3ADC29117B8000C6F7FE /* CalendarEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEvent.swift; sourceTree = "<group>"; };
D8FE3ADF2911817400C6F7FE /* CalendarList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarList.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -131,7 +143,6 @@
D832648428E8EAA300D1C1B7 /* Date.swift */,
D8FA1F442AD79EFE00E18E26 /* EKCalendar.swift */,
D8FA1F462AD79F1D00E18E26 /* EKCalendarItem.swift */,
D81507EB25FDB05100290DD2 /* EKEvent.swift */,
D8805D9F2603A0A700C23C11 /* EKEventStore.swift */,
D8805D9526039D4D00C23C11 /* Int.swift */,
D8A3D3D32911F105002AFC32 /* Set.swift */,
Expand Down Expand Up @@ -246,6 +257,7 @@
D8805D9A26039DED00C23C11 /* CheckboxStyle.swift */,
D8DCDDDA25F664440083DF48 /* ContentView.swift */,
D8DCDE0725F6F9410083DF48 /* MonthView.swift */,
D8BAC11B2B5F0A1800D6A98A /* SettingsView.swift */,
D8DCDE1125F6FD060083DF48 /* YearView.swift */,
);
path = Interface;
Expand All @@ -256,9 +268,15 @@
children = (
D81507E625FD8AF600290DD2 /* ApplicationModel.swift */,
D8B435A129119A96008F4F6F /* CalendarError.swift */,
D8FE3ADC29117B8000C6F7FE /* CalendarEvent.swift */,
D8BAC11D2B5F451C00D6A98A /* CalendarInstance.swift */,
D8FE3ADA291178BF00C6F7FE /* CalendarItem.swift */,
D8FE3ADC29117B8000C6F7FE /* Event.swift */,
D8BAC11F2B5F528300D6A98A /* CalendarStore.swift */,
D8BAC1212B5F531D00D6A98A /* DemoStore.swift */,
D817E2852B60A6CD00346C3C /* MonthlySummary.swift */,
D8BAC1232B5F60EE00D6A98A /* SimilarEvents.swift */,
D8FE3AD82911789500C6F7FE /* Summary.swift */,
D817E2832B609C6D00346C3C /* Weekday.swift */,
D8B4359F29119A5D008F4F6F /* WindowModel.swift */,
);
path = Model;
Expand Down Expand Up @@ -401,16 +419,19 @@
buildActionMask = 2147483647;
files = (
D8DCDDDB25F664440083DF48 /* ContentView.swift in Sources */,
D8BAC11C2B5F0A1800D6A98A /* SettingsView.swift in Sources */,
D817E2842B609C6D00346C3C /* Weekday.swift in Sources */,
D81507DF25FD0D5300290DD2 /* Calendar.swift in Sources */,
D81507EC25FDB05100290DD2 /* EKEvent.swift in Sources */,
D8C296AB2B5777FA00286301 /* URL.swift in Sources */,
D8FE3AE02911817400C6F7FE /* CalendarList.swift in Sources */,
D8FE3AD92911789500C6F7FE /* Summary.swift in Sources */,
D8FE3ADD29117B8000C6F7FE /* Event.swift in Sources */,
D8FE3ADD29117B8000C6F7FE /* CalendarEvent.swift in Sources */,
D8BAC11E2B5F451C00D6A98A /* CalendarInstance.swift in Sources */,
D8DCDE0825F6F9410083DF48 /* MonthView.swift in Sources */,
D8DCDE1225F6FD060083DF48 /* YearView.swift in Sources */,
D8FA1F472AD79F1D00E18E26 /* EKCalendarItem.swift in Sources */,
D8805DA02603A0A700C23C11 /* EKEventStore.swift in Sources */,
D8BAC1242B5F60EE00D6A98A /* SimilarEvents.swift in Sources */,
D81507E725FD8AF600290DD2 /* ApplicationModel.swift in Sources */,
D8B435A229119A96008F4F6F /* CalendarError.swift in Sources */,
D832648528E8EAA300D1C1B7 /* Date.swift in Sources */,
Expand All @@ -421,6 +442,9 @@
D8FE3ADB291178BF00C6F7FE /* CalendarItem.swift in Sources */,
D832648828E8EBF300D1C1B7 /* DateRangeIterator.swift in Sources */,
D8805D9B26039DED00C23C11 /* CheckboxStyle.swift in Sources */,
D817E2862B60A6CD00346C3C /* MonthlySummary.swift in Sources */,
D8BAC1202B5F528300D6A98A /* CalendarStore.swift in Sources */,
D8BAC1222B5F531D00D6A98A /* DemoStore.swift in Sources */,
D8DCDDD925F664440083DF48 /* OverviewApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
2 changes: 0 additions & 2 deletions macos/Overview/Extensions/Calendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ extension Calendar {
return Calendar(identifier: .gregorian)
}

// TODO: Convenience on DateInterval, perhaps?
func dateInterval(start: Date, duration: DateComponents) throws -> DateInterval {
guard let end = date(byAdding: duration, to: start) else {
throw CalendarError.invalidDate
Expand All @@ -39,7 +38,6 @@ extension Calendar {
var date = dateInterval.start
while date < dateInterval.end {
guard let nextDate = self.date(byAdding: components, to: date) else {
// TODO: This is an error?
return
}
block(DateInterval(start: date, end: nextDate))
Expand Down
118 changes: 58 additions & 60 deletions macos/Overview/Extensions/EKEventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,65 +22,7 @@ import EventKit

extension EKEventStore {

func events(dateInterval: DateInterval, calendars: [EKCalendar]?) -> [EKEvent] {
let predicate = predicateForEvents(withStart: dateInterval.start,
end: dateInterval.end,
calendars: calendars)
return events(matching: predicate)
}

func events(calendar: Calendar,
dateInterval: DateInterval,
granularity: DateComponents,
calendars: [EKCalendar]?) throws -> [EKEvent] {
var results: [EKEvent] = []
calendar.enumerate(dateInterval: dateInterval, components: granularity) { dateInterval in
results = results + events(dateInterval: dateInterval, calendars: calendars)
}
return results
}

func summaries(calendar: Calendar,
dateInterval: DateInterval,
granularity: DateComponents,
calendars: [EKCalendar]?) throws -> [Summary<CalendarItem, EKEvent>] {
let events: [EKEvent] = try self.events(calendar: calendar,
dateInterval: dateInterval,
granularity: granularity,
calendars: calendars)
let group = Dictionary(grouping: events) { CalendarItem(calendar: $0.calendar , title: $0.title ?? "") }
var results: [Summary<CalendarItem, EKEvent>] = []
for (context, events) in group {
results.append(Summary(dateInterval: dateInterval, context: context, items: events))
}
return results
}

func summaries(calendar: Calendar,
dateInterval: DateInterval,
calendars: [EKCalendar]) throws -> [Summary<Array<EKCalendar>, Summary<CalendarItem, EKEvent>>] {
var results: [Summary<Array<EKCalendar>, Summary<CalendarItem, EKEvent>>] = []
calendar.enumerate(dateInterval: dateInterval, components: DateComponents(month: 1)) { dateInterval in
let summaries = try! self.summaries(calendar: calendar,
dateInterval: dateInterval,
granularity: DateComponents(month: 1),
calendars: calendars)
results.append(Summary(dateInterval: dateInterval, context: calendars, items: summaries))
}
return results
}

func summary(calendar: Calendar,
year: Int,
calendars: [EKCalendar]) throws -> [Summary<Array<EKCalendar>, Summary<CalendarItem, EKEvent>>] {
guard let start = calendar.date(from: DateComponents(year: year, month: 1)) else {
throw CalendarError.invalidDate
}
let dateInterval = try calendar.dateInterval(start: start, duration: DateComponents(year: 1))
return try summaries(calendar: calendar, dateInterval: dateInterval, calendars: calendars)
}

func earliestEventStartDate(after startDate: Date, before endDate: Date, calendars: [EKCalendar]) -> Date? {
private func earliestEventStartDate(after startDate: Date, before endDate: Date, calendars: [EKCalendar]) -> Date? {
var result: Date? = nil
enumerateEvents(matching: predicateForEvents(withStart: startDate,
end: endDate,
Expand All @@ -94,7 +36,26 @@ extension EKEventStore {
return result
}

func earliestEventStartDate(calendars: [EKCalendar]) -> Date? {
private func earliestEventStartDate(calendars: [CalendarInstance]) -> Date? {
let calendars = self.calendars(calendars: calendars)
guard !calendars.isEmpty else {
return nil
}

// Search backwards for the earliest calendar entry in 4 year intervals (documented EventKit restriction).
var result: Date? = nil
let iterator = DateRangeIterator(date: .now, increment: DateComponents(year: -4))
for (startDate, endDate) in iterator {
guard let date = earliestEventStartDate(after: startDate, before: endDate, calendars: calendars) else {
return result
}
result = date
}
return result
}

private func earliestEventStartDate(calendars: [EKCalendar]) -> Date? {

// Search backwards for the earliest calendar entry in 4 year intervals (documented EventKit restriction).
var result: Date? = nil
let iterator = DateRangeIterator(date: .now, increment: DateComponents(year: -4))
Expand All @@ -107,4 +68,41 @@ extension EKEventStore {
return result
}

private func calendars(calendars: [CalendarInstance]) -> [EKCalendar] {
let calendarIdentifiers = calendars.map { $0.calendarIdentifier }
return calendarIdentifiers.compactMap { calendarIdentifier in
return calendar(withIdentifier: calendarIdentifier)
}
}

}

extension EKEventStore: CalendarStore {

func calendars() -> [CalendarInstance] {
print("Fetching calendars...")
return calendars(for: .event)
.map { CalendarInstance($0) }
}

func activeYears(for calendars: [CalendarInstance]) -> [Int] {
print("Fetching years...")
let contributingCalendars = calendars.filter { $0.type != .birthday }
let earliestDate = earliestEventStartDate(calendars: contributingCalendars) ?? .now
let years = Array((earliestDate.year...Date.now.year).reversed())
return years
}

func events(dateInterval: DateInterval, calendars: [CalendarInstance]) -> [CalendarEvent] {
let calendars = self.calendars(calendars: calendars)
guard !calendars.isEmpty else {
return []
}
let predicate = predicateForEvents(withStart: dateInterval.start,
end: dateInterval.end,
calendars: calendars)
return events(matching: predicate)
.map { CalendarEvent($0) }
}

}
2 changes: 1 addition & 1 deletion macos/Overview/Interface/MonthView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct MonthView: View {

let calendar = Calendar.current

@State var summary: Summary<Array<EKCalendar>, Summary<CalendarItem, EKEvent>>
@State var summary: MonthlySummary

var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
Expand Down
40 changes: 40 additions & 0 deletions macos/Overview/Interface/SettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2021-2024 Jason Morley
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SwiftUI

struct SettingsView: View {

@ObservedObject var applicationModel: ApplicationModel

init(applicationModel: ApplicationModel) {
self.applicationModel = applicationModel
}

var body: some View {
Form {
Section("Developer") {
Toggle("Use Demo Data", isOn: $applicationModel.useDemoData)
}
}
.formStyle(.grouped)
}

}
Loading

0 comments on commit 4055299

Please # to comment.