From afc8ee016064a3199cf6cc090ebe56f2a7dc3ff6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 12 Sep 2026 23:18:05 -0700 Subject: [PATCH] refactor: share EventKit reminder snapshot conversion --- .../RemindCore/EventKitReminderSnapshot.swift | 87 +++++++++++++ Sources/RemindCore/EventKitStore.swift | 119 +----------------- 2 files changed, 90 insertions(+), 116 deletions(-) create mode 100644 Sources/RemindCore/EventKitReminderSnapshot.swift diff --git a/Sources/RemindCore/EventKitReminderSnapshot.swift b/Sources/RemindCore/EventKitReminderSnapshot.swift new file mode 100644 index 0000000..ddee1dc --- /dev/null +++ b/Sources/RemindCore/EventKitReminderSnapshot.swift @@ -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) + } +} diff --git a/Sources/RemindCore/EventKitStore.swift b/Sources/RemindCore/EventKitStore.swift index b9ff41d..06fe009 100644 --- a/Sources/RemindCore/EventKitStore.swift +++ b/Sources/RemindCore/EventKitStore.swift @@ -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 } @@ -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( @@ -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 { @@ -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) @@ -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) @@ -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