// // CalendarAsync.swift // RWPureSwift // // Created by Christopher Hotchkiss on 2/25/26. // import AppTypes @preconcurrency import EventKit import Dependencies import DependenciesMacros #if canImport(UIKit) import UIKit #endif @DependencyClient public struct CalendarAsync: Sendable { public var calendarAccess: @Sendable () -> EKAuthorizationStatus = { .notDetermined } public var openCalendarSettings: @Sendable () async -> () = {} public var requestAccess: @Sendable () async throws -> Bool = { false } public var getCalendars: @Sendable () -> [EKCalendar] = { [] } public var getActiveEvent: @Sendable (CalendarId, Date) -> EKEvent? public var getNextEvent: @Sendable (CalendarId, Date) -> EKEvent? /// First calendar matching the synced descriptor (title AND source), if any. public var findCalendar: @Sendable (CalendarDescriptor) -> CalendarId? /// Descriptor for a locally-cached id; nil when the calendar no longer exists. public var calendarDescriptor: @Sendable (CalendarId) -> CalendarDescriptor? } extension CalendarAsync: DependencyKey { public static var liveValue: Self { nonisolated(unsafe) let store = EKEventStore() return Self( calendarAccess: { return EKEventStore.authorizationStatus(for: .event) }, openCalendarSettings: { @Dependency(\.fireAndForget) var fireAndForget await fireAndForget { @MainActor in #if canImport(UIKit) #if targetEnvironment(macCatalyst) let url = "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars" await UIApplication.shared.open(URL(string: url)!) #else let url = UIApplication.openSettingsURLString UIApplication.shared.open(URL(string: url)!) #endif #endif } }, requestAccess: { return try await store.requestFullAccessToEvents() }, getCalendars: { return store.calendars(for: .event) }, getActiveEvent: { calendarId, currentTime in guard let calendar = store.calendar(withIdentifier: calendarId.rawValue) else { return nil } let startDate = Date(timeInterval: TimeInterval(-60*60*24), since: currentTime) let endDate = Date(timeInterval: TimeInterval(60*60*24), since: currentTime) let predicate = store.predicateForEvents(withStart: startDate, end: endDate, calendars: [calendar]) let events = store.events(matching: predicate) for e in events { if e.startDate <= currentTime && e.endDate >= currentTime{ return e } } return nil }, getNextEvent: { calendarId, currentTime in guard let calendar = store.calendar(withIdentifier: calendarId.rawValue) else { return nil } //Look for events within the next hour let startDate = Date(timeInterval: TimeInterval(1), since: currentTime) let endDate = Date(timeInterval: TimeInterval(60*60), since: currentTime) let predicate = store.predicateForEvents(withStart: startDate, end: endDate, calendars: [calendar]) let events = store.events(matching: predicate) var nextEvent: EKEvent? = nil for e in events{ if e.startDate < currentTime { continue } if nextEvent == nil { nextEvent = e } else if nextEvent!.startDate > e.startDate { nextEvent = e } } return nextEvent }, findCalendar: { descriptor in store.calendars(for: .event) .first { $0.title == descriptor.title && ($0.source?.title ?? "") == descriptor.sourceTitle } .map { CalendarId($0.calendarIdentifier) } }, calendarDescriptor: { calendarId in store.calendar(withIdentifier: calendarId.rawValue).map { CalendarDescriptor(title: $0.title, sourceTitle: $0.source?.title ?? "") } } ) } } extension CalendarAsync: TestDependencyKey { public static let testValue = Self() public static var previewValue: Self { return Self( calendarAccess: { return .fullAccess }, openCalendarSettings: {}, requestAccess: { return true }, getCalendars: { return [] }, getActiveEvent: { cal, date in return nil }, getNextEvent: { cal, date in return nil }, findCalendar: { _ in return nil }, calendarDescriptor: { _ in return nil } ) } } extension DependencyValues { public var calendarAsync: CalendarAsync { get { self[CalendarAsync.self] } set { self[CalendarAsync.self] = newValue } } }