spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1//2// EventKitBridge.swift3// Poche4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//89import Foundation10import EventKit1112enum BridgeError: Error {13 case remindersAccessDenied14 case calendarAccessDenied1516 var userMessage: String {17 switch self {18 case .remindersAccessDenied:19 "Poche n'a pas accès aux Rappels. Autorise l'accès dans Réglages → Confidentialité → Rappels."20 case .calendarAccessDenied:21 "Poche n'a pas accès au Calendrier. Autorise l'accès dans Réglages → Confidentialité → Calendriers."22 }23 }24}2526/// EventKit bridge. Writes are reachable only through ActionExecutor after27/// user confirmation. Permissions are requested at the moment of need,28/// never at launch (CLAUDE.md §4).29@MainActor30final class EventKitBridge {31 private let eventStore = EKEventStore()3233 // MARK: - Permissions3435 private func ensureRemindersAccess() async throws {36 switch EKEventStore.authorizationStatus(for: .reminder) {37 case .fullAccess:38 return39 case .notDetermined:40 let granted = (try? await eventStore.requestFullAccessToReminders()) ?? false41 guard granted else { throw BridgeError.remindersAccessDenied }42 default:43 throw BridgeError.remindersAccessDenied44 }45 }4647 private func ensureCalendarAccess() async throws {48 switch EKEventStore.authorizationStatus(for: .event) {49 case .fullAccess:50 return51 case .notDetermined:52 let granted = (try? await eventStore.requestFullAccessToEvents()) ?? false53 guard granted else { throw BridgeError.calendarAccessDenied }54 default:55 throw BridgeError.calendarAccessDenied56 }57 }5859 // MARK: - Writes (ActionExecutor only)6061 func createReminder(_ draft: ReminderDraft) async throws {62 try await ensureRemindersAccess()6364 let reminder = EKReminder(eventStore: eventStore)65 reminder.title = draft.title66 reminder.calendar = remindersList(named: draft.list)67 ?? eventStore.defaultCalendarForNewReminders()68 reminder.dueDateComponents = Calendar.current.dateComponents(69 [.year, .month, .day, .hour, .minute],70 from: draft.due71 )72 reminder.addAlarm(EKAlarm(absoluteDate: draft.due))73 try eventStore.save(reminder, commit: true)74 }7576 func createEvent(_ draft: EventDraft) async throws {77 try await ensureCalendarAccess()7879 let event = EKEvent(eventStore: eventStore)80 event.title = draft.title81 event.startDate = draft.start82 event.endDate = draft.end83 event.location = draft.location84 event.calendar = eventStore.defaultCalendarForNewEvents85 try eventStore.save(event, span: .thisEvent, commit: true)86 }8788 // MARK: - Reads8990 /// Output is budgeted: at most `limit` lines, one short line each91 /// (a tool returning 30 events kills the session — CLAUDE.md §4).92 func upcomingEvents(days: Int, limit: Int) async throws -> [String] {93 try await ensureCalendarAccess()9495 let start = Date.now96 let end = Calendar.current.date(byAdding: .day, value: days, to: start) ?? start97 let predicate = eventStore.predicateForEvents(withStart: start, end: end, calendars: nil)98 return eventStore.events(matching: predicate)99 .sorted { $0.startDate < $1.startDate }100 .prefix(limit)101 .map { event in102 let when = event.startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute())103 return "• \(when) — \(event.title ?? "Sans titre")"104 }105 }106107 func upcomingReminders(limit: Int) async throws -> [String] {108 try await ensureRemindersAccess()109110 let predicate = eventStore.predicateForIncompleteReminders(111 withDueDateStarting: nil,112 ending: nil,113 calendars: nil114 )115 // EKReminder is not Sendable: render to plain strings inside the116 // fetch callback instead of carrying objects across the continuation.117 return await withCheckedContinuation { continuation in118 eventStore.fetchReminders(matching: predicate) { result in119 let lines = (result ?? [])120 .prefix(limit)121 .map { reminder in122 let due = reminder.dueDateComponents?.date.map {123 " — " + $0.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute())124 } ?? ""125 return "• \(reminder.title ?? "Sans titre")\(due)"126 }127 continuation.resume(returning: Array(lines))128 }129 }130 }131132 private func remindersList(named name: String?) -> EKCalendar? {133 guard let name else { return nil }134 return eventStore.calendars(for: .reminder)135 .first { $0.title.compare(name, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame }136 }137}138