Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions Sources/RemindCore/EventKitReminderSnapshot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import EventKit
import Foundation

extension RemindersStore {
struct ReminderData: Sendable {
let id: String
let title: String
let notes: String?
let url: URL?
let isCompleted: Bool
let completionDate: Date?
let creationDate: Date?
let lastModifiedDate: Date?
let priority: Int
let dueDateComponents: DateComponents?
let dueDateIsAllDay: Bool
let alarmDate: Date?
let recurrenceRule: RecurrenceRule?
let locationTrigger: LocationTrigger?
let listID: String
let listName: String

func item(calendar: Calendar) -> ReminderItem {
return ReminderItem(
id: id,
title: title,
notes: notes,
url: url,
isCompleted: isCompleted,
completionDate: completionDate,
creationDate: creationDate,
lastModifiedDate: lastModifiedDate,
priority: ReminderPriority(eventKitValue: priority),
dueDate: dueDateComponents.flatMap { calendar.date(from: $0) },
dueDateIsAllDay: dueDateIsAllDay,
alarmDate: alarmDate,
recurrenceRule: recurrenceRule,
locationTrigger: locationTrigger,
listID: listID,
listName: listName
)
}
}

static func reminderItem(from reminder: EKReminder, calendar: Calendar = .current) throws -> ReminderItem {
guard let data = reminderData(from: reminder) else {
throw RemindCoreError.operationFailed("Reminder is missing a calendar")
}
return data.item(calendar: calendar)
}

static func reminderData(from reminder: EKReminder) -> ReminderData? {
// Skip orphaned reminders before dereferencing EventKit's calendar IUO.
guard let calendar = reminder.calendar else { return nil }
let components = reminder.dueDateComponents
return ReminderData(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
url: reminder.url,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
creationDate: reminder.creationDate,
lastModifiedDate: reminder.lastModifiedDate,
priority: Int(reminder.priority),
dueDateComponents: components,
dueDateIsAllDay: isAllDay(components),
alarmDate: alarmDate(from: reminder),
recurrenceRule: recurrenceRule(from: reminder),
locationTrigger: locationTrigger(from: reminder),
listID: calendar.calendarIdentifier,
listName: calendar.title
)
}

private static func alarmDate(from reminder: EKReminder) -> Date? {
reminder.alarms?
.compactMap(\.absoluteDate)
.min()
}

private static func recurrenceRule(from reminder: EKReminder) -> RecurrenceRule? {
guard let rule = reminder.recurrenceRules?.first else { return nil }
guard let frequency = RecurrenceFrequency(eventKitFrequency: rule.frequency) else { return nil }
return RecurrenceRule(frequency: frequency, interval: rule.interval)
}
}
119 changes: 3 additions & 116 deletions Sources/RemindCore/EventKitStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ public actor RemindersStore {
}

public func resolveList(_ target: ReminderListTarget) async throws -> ReminderList {
let lists = await lists()
switch target {
case .name(let name):
return try ListResolver.resolve(name, in: lists)
case .id(let id):
return try ListResolver.resolveID(id, in: lists)
}
try resolvedList(matching: target, in: await lists())
}

public func defaultListName() -> String? { defaultList()?.title }
Expand Down Expand Up @@ -220,73 +214,6 @@ extension RemindersStore {
}
}

struct ReminderData: Sendable {
let id: String
let title: String
let notes: String?
let url: URL?
let isCompleted: Bool
let completionDate: Date?
let creationDate: Date?
let lastModifiedDate: Date?
let priority: Int
let dueDateComponents: DateComponents?
let dueDateIsAllDay: Bool
let alarmDate: Date?
let recurrenceRule: RecurrenceRule?
let locationTrigger: LocationTrigger?
let listID: String
let listName: String
}

static func reminderItem(from reminder: EKReminder, calendar: Calendar = .current) throws -> ReminderItem {
guard let data = reminderData(from: reminder) else {
throw RemindCoreError.operationFailed("Reminder is missing a calendar")
}
return ReminderItem(
id: data.id,
title: data.title,
notes: data.notes,
url: data.url,
isCompleted: data.isCompleted,
completionDate: data.completionDate,
creationDate: data.creationDate,
lastModifiedDate: data.lastModifiedDate,
priority: ReminderPriority(eventKitValue: data.priority),
dueDate: data.dueDateComponents.flatMap { calendar.date(from: $0) },
dueDateIsAllDay: data.dueDateIsAllDay,
alarmDate: data.alarmDate,
recurrenceRule: data.recurrenceRule,
locationTrigger: data.locationTrigger,
listID: data.listID,
listName: data.listName
)
}

static func reminderData(from reminder: EKReminder) -> ReminderData? {
// Skip orphaned reminders before dereferencing EventKit's calendar IUO.
guard let calendar = reminder.calendar else { return nil }
let components = reminder.dueDateComponents
return ReminderData(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
url: reminder.url,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
creationDate: reminder.creationDate,
lastModifiedDate: reminder.lastModifiedDate,
priority: Int(reminder.priority),
dueDateComponents: components,
dueDateIsAllDay: isAllDay(components),
alarmDate: alarmDate(from: reminder),
recurrenceRule: recurrenceRule(from: reminder),
locationTrigger: locationTrigger(from: reminder),
listID: calendar.calendarIdentifier,
listName: calendar.title
)
}

private func fetchReminders(in calendars: [EKCalendar]) async throws -> [ReminderItem] {
let context = EventKitFetchContext(eventStore: eventStore, calendars: calendars)
let reminderData: [ReminderData] = try await AsyncTimeout.withTimeout(
Expand All @@ -302,26 +229,7 @@ extension RemindersStore {
return { request.cancel() }
}

return reminderData.map { data in
ReminderItem(
id: data.id,
title: data.title,
notes: data.notes,
url: data.url,
isCompleted: data.isCompleted,
completionDate: data.completionDate,
creationDate: data.creationDate,
lastModifiedDate: data.lastModifiedDate,
priority: ReminderPriority(eventKitValue: data.priority),
dueDate: date(from: data.dueDateComponents),
dueDateIsAllDay: data.dueDateIsAllDay,
alarmDate: data.alarmDate,
recurrenceRule: data.recurrenceRule,
locationTrigger: data.locationTrigger,
listID: data.listID,
listName: data.listName
)
}
return reminderData.map { $0.item(calendar: calendar) }
}

private func reminder(withID id: String) throws -> EKReminder {
Expand All @@ -331,10 +239,6 @@ extension RemindersStore {
return item
}

private func calendar(named name: String) throws -> EKCalendar {
try calendar(matching: .name(name))
}

private func calendar(matching target: ReminderListTarget) throws -> EKCalendar {
let resolved = try resolvedList(matching: target)
let calendars = eventStore.calendars(for: .reminder)
Expand Down Expand Up @@ -387,21 +291,10 @@ extension RemindersStore {
return result
}

private func date(from components: DateComponents?) -> Date? {
guard let components else { return nil }
return calendar.date(from: components)
}

private func item(from reminder: EKReminder) throws -> ReminderItem {
try Self.reminderItem(from: reminder, calendar: calendar)
}

private static func alarmDate(from reminder: EKReminder) -> Date? {
reminder.alarms?
.compactMap(\.absoluteDate)
.min()
}

private func replaceRecurrence(on reminder: EKReminder, with rule: RecurrenceRule?) {
for existing in reminder.recurrenceRules ?? [] {
reminder.removeRecurrenceRule(existing)
Expand All @@ -411,16 +304,10 @@ extension RemindersStore {
EKRecurrenceRule(recurrenceWith: rule.eventKitFrequency, interval: rule.interval, end: nil))
}

private static func recurrenceRule(from reminder: EKReminder) -> RecurrenceRule? {
guard let rule = reminder.recurrenceRules?.first else { return nil }
guard let frequency = RecurrenceFrequency(eventKitFrequency: rule.frequency) else { return nil }
return RecurrenceRule(frequency: frequency, interval: rule.interval)
}

}

extension RecurrenceFrequency {
fileprivate init?(eventKitFrequency: EKRecurrenceFrequency) {
init?(eventKitFrequency: EKRecurrenceFrequency) {
switch eventKitFrequency {
case .daily:
self = .daily
Expand Down
Loading