Christopher Hotchkiss Christopher Hotchkiss
Login

Code

remindwall / RWPureSwift / Sources / CalendarAsync / CalendarAsync.swift

Back to remindwall — Current RemindWall

Download this file

//
//  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 }
  }
}