remindctl/Sources/RemindCore/EventKitStore.swift
2026-01-03 13:20:56 +01:00

256 lines
8.3 KiB
Swift

import EventKit
import Foundation
public actor RemindersStore {
private let eventStore = EKEventStore()
private let calendar: Calendar
public init(calendar: Calendar = .current) {
self.calendar = calendar
}
public func requestAccess() async throws {
let status = Self.authorizationStatus()
switch status {
case .notDetermined:
let updated = try await requestAuthorization()
if updated != .fullAccess {
throw RemindCoreError.accessDenied
}
case .denied, .restricted:
throw RemindCoreError.accessDenied
case .writeOnly:
throw RemindCoreError.writeOnlyAccess
case .fullAccess:
break
}
}
public static func authorizationStatus() -> RemindersAuthorizationStatus {
RemindersAuthorizationStatus(eventKitStatus: EKEventStore.authorizationStatus(for: .reminder))
}
public func requestAuthorization() async throws -> RemindersAuthorizationStatus {
let status = Self.authorizationStatus()
switch status {
case .notDetermined:
let granted = try await requestFullAccess()
return granted ? .fullAccess : .denied
default:
return status
}
}
public func lists() async -> [ReminderList] {
eventStore.calendars(for: .reminder).map { calendar in
ReminderList(id: calendar.calendarIdentifier, title: calendar.title)
}
}
public func defaultListName() -> String? {
eventStore.defaultCalendarForNewReminders()?.title
}
public func reminders(in listName: String? = nil) async throws -> [ReminderItem] {
let calendars: [EKCalendar]
if let listName {
calendars = eventStore.calendars(for: .reminder).filter { $0.title == listName }
if calendars.isEmpty {
throw RemindCoreError.listNotFound(listName)
}
} else {
calendars = eventStore.calendars(for: .reminder)
}
return await fetchReminders(in: calendars)
}
public func createList(name: String) async throws -> ReminderList {
let list = EKCalendar(for: .reminder, eventStore: eventStore)
list.title = name
guard let source = eventStore.defaultCalendarForNewReminders()?.source else {
throw RemindCoreError.operationFailed("Unable to determine default reminder source")
}
list.source = source
try eventStore.saveCalendar(list, commit: true)
return ReminderList(id: list.calendarIdentifier, title: list.title)
}
public func renameList(oldName: String, newName: String) async throws {
let calendar = try calendar(named: oldName)
guard calendar.allowsContentModifications else {
throw RemindCoreError.operationFailed("Cannot modify system list")
}
calendar.title = newName
try eventStore.saveCalendar(calendar, commit: true)
}
public func deleteList(name: String) async throws {
let calendar = try calendar(named: name)
guard calendar.allowsContentModifications else {
throw RemindCoreError.operationFailed("Cannot delete system list")
}
try eventStore.removeCalendar(calendar, commit: true)
}
public func createReminder(_ draft: ReminderDraft, listName: String) async throws -> ReminderItem {
let calendar = try calendar(named: listName)
let reminder = EKReminder(eventStore: eventStore)
reminder.title = draft.title
reminder.notes = draft.notes
reminder.calendar = calendar
reminder.priority = draft.priority.eventKitValue
if let dueDate = draft.dueDate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
}
try eventStore.save(reminder, commit: true)
return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
}
public func updateReminder(id: String, update: ReminderUpdate) async throws -> ReminderItem {
let reminder = try reminder(withID: id)
if let title = update.title {
reminder.title = title
}
if let notes = update.notes {
reminder.notes = notes
}
if let dueDateUpdate = update.dueDate {
if let dueDate = dueDateUpdate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
} else {
reminder.dueDateComponents = nil
}
}
if let priority = update.priority {
reminder.priority = priority.eventKitValue
}
if let listName = update.listName {
reminder.calendar = try calendar(named: listName)
}
if let isCompleted = update.isCompleted {
reminder.isCompleted = isCompleted
}
try eventStore.save(reminder, commit: true)
return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
}
public func completeReminders(ids: [String]) async throws -> [ReminderItem] {
var updated: [ReminderItem] = []
for id in ids {
let reminder = try reminder(withID: id)
reminder.isCompleted = true
try eventStore.save(reminder, commit: true)
updated.append(
ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
)
}
return updated
}
public func deleteReminders(ids: [String]) async throws -> Int {
var deleted = 0
for id in ids {
let reminder = try reminder(withID: id)
try eventStore.remove(reminder, commit: true)
deleted += 1
}
return deleted
}
private func requestFullAccess() async throws -> Bool {
try await withCheckedThrowingContinuation { continuation in
eventStore.requestFullAccessToReminders { granted, error in
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: granted)
}
}
}
private func fetchReminders(in calendars: [EKCalendar]) async -> [ReminderItem] {
await withCheckedContinuation { continuation in
let predicate = eventStore.predicateForReminders(in: calendars)
eventStore.fetchReminders(matching: predicate) { reminders in
let mapped = (reminders ?? []).map { reminder in
self.item(from: reminder)
}
continuation.resume(returning: mapped)
}
}
}
private func reminder(withID id: String) throws -> EKReminder {
guard let item = eventStore.calendarItem(withIdentifier: id) as? EKReminder else {
throw RemindCoreError.reminderNotFound(id)
}
return item
}
private func calendar(named name: String) throws -> EKCalendar {
let calendars = eventStore.calendars(for: .reminder).filter { $0.title == name }
guard let calendar = calendars.first else {
throw RemindCoreError.listNotFound(name)
}
return calendar
}
private func calendarComponents(from date: Date) -> DateComponents {
calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
}
private func date(from components: DateComponents?) -> Date? {
guard let components else { return nil }
return calendar.date(from: components)
}
private func item(from reminder: EKReminder) -> ReminderItem {
ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
}
}