From 7e2e610f81c11c7ebab667e6246664e1d168869c Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 3 Dec 2025 11:11:33 +0530 Subject: [PATCH] Partial BugFix: Calendar --- .../components/Settings/SettingsView.swift | 4 +- DynamicIsland/managers/CalendarManager.swift | 108 +++++----- .../ReminderLiveActivityManager.swift | 184 +++++++++++++++--- 3 files changed, 203 insertions(+), 93 deletions(-) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 90a3b0b..8dea404 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1784,8 +1784,8 @@ struct CalendarSettings: View { HStack { Button("Request Access") { Task { - await calendarManager.checkCalendarAuthorization(forceReload: true) - await calendarManager.checkReminderAuthorization(forceReload: true) + await calendarManager.checkCalendarAuthorization() + await calendarManager.checkReminderAuthorization() } } .buttonStyle(.borderedProminent) diff --git a/DynamicIsland/managers/CalendarManager.swift b/DynamicIsland/managers/CalendarManager.swift index de2ecfa..0c35a69 100644 --- a/DynamicIsland/managers/CalendarManager.swift +++ b/DynamicIsland/managers/CalendarManager.swift @@ -23,19 +23,21 @@ class CalendarManager: ObservableObject { @Published var selectedCalendarIDs: Set = [] @Published var calendarAuthorizationStatus: EKAuthorizationStatus = .notDetermined @Published var reminderAuthorizationStatus: EKAuthorizationStatus = .notDetermined + private var selectedCalendars: [CalendarModel] = [] private let calendarService = CalendarService() - + private var lastEventsFetchDate: Date? + private let reloadRefreshInterval: TimeInterval = 15 private var eventStoreChangedObserver: NSObjectProtocol? var hasCalendarAccess: Bool { isAuthorized(calendarAuthorizationStatus) } var hasReminderAccess: Bool { isAuthorized(reminderAuthorizationStatus) } private init() { - self.currentWeekStartDate = CalendarManager.startOfDay(Date()) + currentWeekStartDate = CalendarManager.startOfDay(Date()) setupEventStoreChangedObserver() Task { - await self.bootstrapAuthorizations() + await reloadCalendarAndReminderLists() } } @@ -52,98 +54,84 @@ class CalendarManager: ObservableObject { queue: .main ) { [weak self] _ in Task { - await self?.reloadCalendarAndReminderLists() + guard let self else { return } + await self.reloadCalendarAndReminderLists() + await self.maybeRefreshEventsAfterReload() } } } @MainActor func reloadCalendarAndReminderLists() async { - let all = await calendarService.calendars() - self.eventCalendars = all.filter { !$0.isReminder } - self.reminderLists = all.filter { $0.isReminder } - self.allCalendars = all // for legacy compatibility, can be removed if not needed + let allCalendars = await calendarService.calendars() + eventCalendars = allCalendars.filter { !$0.isReminder } + reminderLists = allCalendars.filter { $0.isReminder } + self.allCalendars = allCalendars updateSelectedCalendars() + } + + @MainActor + private func maybeRefreshEventsAfterReload() async { + guard hasCalendarAccess else { return } + let now = Date() + if let lastFetch = lastEventsFetchDate, now.timeIntervalSince(lastFetch) < reloadRefreshInterval { + return + } await updateEvents() } - private func bootstrapAuthorizations() async { - await checkCalendarAuthorization(forceReload: true) - await checkReminderAuthorization(forceReload: true) - } - - private func refreshAuthorizationStatuses() { - calendarAuthorizationStatus = EKEventStore.authorizationStatus(for: .event) - reminderAuthorizationStatus = EKEventStore.authorizationStatus(for: .reminder) - } - private func isAuthorized(_ status: EKAuthorizationStatus) -> Bool { switch status { - case .fullAccess, .authorized: + case .authorized, .fullAccess: return true default: return false } } - func checkCalendarAuthorization(forceReload: Bool = false) async { - refreshAuthorizationStatuses() - let status = calendarAuthorizationStatus - print("📅 Current calendar authorization status: \(status.rawValue)") + func checkCalendarAuthorization() async { + let status = EKEventStore.authorizationStatus(for: .event) + calendarAuthorizationStatus = status switch status { case .notDetermined: let granted = await calendarService.requestAccess(to: .event) - refreshAuthorizationStatuses() - if granted || hasCalendarAccess { + calendarAuthorizationStatus = granted ? .fullAccess : .denied + if granted { await reloadCalendarAndReminderLists() await updateEvents() } case .restricted, .denied: NSLog("Calendar access denied or restricted") - case .writeOnly: - NSLog("Calendar write-only access") - case .fullAccess: - guard forceReload else { return } + case .authorized, .fullAccess: await reloadCalendarAndReminderLists() await updateEvents() + case .writeOnly: + NSLog("Calendar write only") @unknown default: - if isAuthorized(status) { - guard forceReload else { return } - await reloadCalendarAndReminderLists() - await updateEvents() - } else { - print("Unknown authorization status: \(status.rawValue)") - } + NSLog("Unknown calendar authorization status") } } - func checkReminderAuthorization(forceReload: Bool = false) async { - refreshAuthorizationStatuses() - let status = reminderAuthorizationStatus - print("📅 Current reminder authorization status: \(status.rawValue)") + func checkReminderAuthorization() async { + let status = EKEventStore.authorizationStatus(for: .reminder) + reminderAuthorizationStatus = status switch status { case .notDetermined: let granted = await calendarService.requestAccess(to: .reminder) - refreshAuthorizationStatuses() - if granted || hasReminderAccess { + reminderAuthorizationStatus = granted ? .fullAccess : .denied + if granted { await reloadCalendarAndReminderLists() } case .restricted, .denied: NSLog("Reminder access denied or restricted") - case .writeOnly: - NSLog("Reminder write-only access") - case .fullAccess: - guard forceReload else { return } + case .authorized, .fullAccess: await reloadCalendarAndReminderLists() + case .writeOnly: + NSLog("Reminder write only") @unknown default: - if isAuthorized(status) { - guard forceReload else { return } - await reloadCalendarAndReminderLists() - } else { - print("Unknown reminder authorization status: \(status.rawValue)") - } + NSLog("Unknown reminder authorization status") } } @@ -171,7 +159,6 @@ class CalendarManager: ObservableObject { let identifiers = Set(allCalendars.map { $0.id }).subtracting([calendar.id]) selectionState = .selected(identifiers) } - case .selected(var identifiers): if isSelected { identifiers.insert(calendar.id) @@ -179,9 +166,11 @@ class CalendarManager: ObservableObject { identifiers.remove(calendar.id) } - selectionState = - identifiers.isEmpty - ? .all : identifiers.count == allCalendars.count ? .all : .selected(identifiers) // if empty, select all + if identifiers.isEmpty || identifiers.count == allCalendars.count { + selectionState = .all + } else { + selectionState = .selected(identifiers) + } } Defaults[.calendarSelectionState] = selectionState @@ -190,7 +179,7 @@ class CalendarManager: ObservableObject { } static func startOfDay(_ date: Date) -> Date { - return Calendar.current.startOfDay(for: date) + Calendar.current.startOfDay(for: date) } func updateCurrentDate(_ date: Date) async { @@ -200,12 +189,13 @@ class CalendarManager: ObservableObject { private func updateEvents() async { let calendarIDs = selectedCalendars.map { $0.id } - let eventsResult = await calendarService.events( + let events = await calendarService.events( from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: calendarIDs ) - self.events = eventsResult + self.events = events + lastEventsFetchDate = Date() } func setReminderCompleted(reminderID: String, completed: Bool) async { diff --git a/DynamicIsland/managers/ReminderLiveActivityManager.swift b/DynamicIsland/managers/ReminderLiveActivityManager.swift index 90b7240..ff4726e 100644 --- a/DynamicIsland/managers/ReminderLiveActivityManager.swift +++ b/DynamicIsland/managers/ReminderLiveActivityManager.swift @@ -41,6 +41,19 @@ final class ReminderLiveActivityManager: ObservableObject { private var evaluationTask: Task? private var hasShownCriticalSneakPeek = false private var latestEvents: [EventModel] = [] + private var pendingEventsSnapshot: [EventModel]? = nil + private var eventsUpdateDebounceTask: Task? { didSet { oldValue?.cancel() } } + private var upcomingComputationTask: Task? { didSet { oldValue?.cancel() } } + private let eventsDebounceInterval: TimeInterval = 0.35 + private var settingsUpdateTask: Task? { didSet { oldValue?.cancel() } } + private var pendingSettingsAction: (() -> Void)? + private var pendingSettingsReason: String? + private let settingsUpdateDebounceInterval: TimeInterval = 0.2 + private var suppressUpdatesForLock = false + + private var lastAppliedLeadTime = Defaults[.reminderLeadTime] + private var lastAppliedHideAllDay = Defaults[.hideAllDayEvents] + private var lastAppliedHideCompleted = Defaults[.hideCompletedReminders] private let calendarManager = CalendarManager.shared @@ -67,21 +80,35 @@ final class ReminderLiveActivityManager: ObservableObject { .store(in: &cancellables) Defaults.publisher(.reminderLeadTime, options: []) - .sink { [weak self] _ in + .map(\.newValue) + .sink { [weak self] newValue in guard let self else { return } - self.recalculateUpcomingEntries(reason: "lead-time") + guard newValue != self.lastAppliedLeadTime else { return } + self.scheduleSettingsRecalculation(reason: "lead-time") { + self.lastAppliedLeadTime = newValue + } } .store(in: &cancellables) Defaults.publisher(.hideAllDayEvents, options: []) - .sink { [weak self] _ in - self?.recalculateUpcomingEntries(reason: "hide-all-day") + .map(\.newValue) + .sink { [weak self] newValue in + guard let self else { return } + guard newValue != self.lastAppliedHideAllDay else { return } + self.scheduleSettingsRecalculation(reason: "hide-all-day") { + self.lastAppliedHideAllDay = newValue + } } .store(in: &cancellables) Defaults.publisher(.hideCompletedReminders, options: []) - .sink { [weak self] _ in - self?.recalculateUpcomingEntries(reason: "hide-completed") + .map(\.newValue) + .sink { [weak self] newValue in + guard let self else { return } + guard newValue != self.lastAppliedHideCompleted else { return } + self.scheduleSettingsRecalculation(reason: "hide-completed") { + self.lastAppliedHideCompleted = newValue + } } .store(in: &cancellables) @@ -96,12 +123,40 @@ final class ReminderLiveActivityManager: ObservableObject { .store(in: &cancellables) calendarManager.$events + .removeDuplicates() .receive(on: RunLoop.main) .sink { [weak self] events in - guard let self else { return } - self.handleCalendarEventsUpdate(events) + self?.handleCalendarEventsUpdate(events) } .store(in: &cancellables) + + LockScreenManager.shared.$isLocked + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] locked in + self?.handleLockStateChange(isLocked: locked) + } + .store(in: &cancellables) + } + + private func scheduleSettingsRecalculation(reason: String, action: @escaping () -> Void) { + pendingSettingsAction = action + pendingSettingsReason = reason + settingsUpdateTask = Task { [weak self] in + guard let self else { return } + let delay = UInt64(settingsUpdateDebounceInterval * 1_000_000_000) + try? await Task.sleep(nanoseconds: delay) + await self.applyPendingSettingsRecalculation() + } + } + + @MainActor + private func applyPendingSettingsRecalculation() { + guard let action = pendingSettingsAction, let reason = pendingSettingsReason else { return } + pendingSettingsAction = nil + pendingSettingsReason = nil + action() + recalculateUpcomingEntries(reason: reason) } private func cancelAllTimers() { @@ -120,28 +175,50 @@ final class ReminderLiveActivityManager: ObservableObject { } private func handleCalendarEventsUpdate(_ events: [EventModel]) { - latestEvents = events + pendingEventsSnapshot = events + guard !suppressUpdatesForLock else { return } + schedulePendingEventsSnapshotApplication() + } + + private func schedulePendingEventsSnapshotApplication() { + eventsUpdateDebounceTask = Task { [weak self] in + guard let self else { return } + let delay = UInt64(eventsDebounceInterval * 1_000_000_000) + try? await Task.sleep(nanoseconds: delay) + guard !Task.isCancelled else { return } + await self.applyPendingEventsSnapshot() + } + } + + @MainActor + private func applyPendingEventsSnapshot() { + guard !suppressUpdatesForLock else { return } + guard let snapshot = pendingEventsSnapshot else { return } + pendingEventsSnapshot = nil + guard snapshot != latestEvents else { return } + + latestEvents = snapshot guard Defaults[.enableReminderLiveActivity] else { return } - logger.debug("[Reminder] Applying calendar snapshot update (events=\(events.count, privacy: .public))") + logger.debug("[Reminder] Applying calendar snapshot update (events=\(snapshot.count, privacy: .public))") recalculateUpcomingEntries(reason: "calendar-events") } - private func shouldHide(_ event: EventModel) -> Bool { - if event.isAllDay && Defaults[.hideAllDayEvents] { - return true + private func handleLockStateChange(isLocked: Bool) { + suppressUpdatesForLock = isLocked + if isLocked { + eventsUpdateDebounceTask = nil + upcomingComputationTask = nil + settingsUpdateTask = nil + pendingSettingsAction = nil + pendingSettingsReason = nil + deactivateReminder() + } else { + if pendingEventsSnapshot != nil { + schedulePendingEventsSnapshotApplication() + } else { + recalculateUpcomingEntries(reason: "lock-resume") + } } - if case let .reminder(completed) = event.type, - completed && Defaults[.hideCompletedReminders] { - return true - } - return false - } - - private func makeEntry(from event: EventModel, leadMinutes: Int, referenceDate: Date) -> ReminderEntry? { - guard event.start > referenceDate else { return nil } - let leadSeconds = max(1, leadMinutes) * 60 - let trigger = event.start.addingTimeInterval(TimeInterval(-leadSeconds)) - return ReminderEntry(event: event, triggerDate: trigger, leadTime: TimeInterval(leadSeconds)) } private func recalculateUpcomingEntries(referenceDate: Date = Date(), reason: String) { @@ -149,18 +226,33 @@ final class ReminderLiveActivityManager: ObservableObject { deactivateReminder() return } + let snapshot = latestEvents + let leadMinutes = Defaults[.reminderLeadTime] + let hideAllDay = Defaults[.hideAllDayEvents] + let hideCompleted = Defaults[.hideCompletedReminders] - guard calendarManager.hasReminderAccess else { + upcomingComputationTask = Task.detached(priority: .utility) { [weak self] in + guard let self else { return } + guard !Task.isCancelled else { return } + let upcoming = Self.buildUpcomingEntries( + events: snapshot, + leadMinutes: leadMinutes, + referenceDate: referenceDate, + hideAllDayEvents: hideAllDay, + hideCompletedReminders: hideCompleted + ) + guard !Task.isCancelled else { return } + await self.publishUpcomingEntries(upcoming, referenceDate: referenceDate, reason: reason) + } + } + + @MainActor + private func publishUpcomingEntries(_ upcoming: [ReminderEntry], referenceDate: Date, reason: String) { + guard Defaults[.enableReminderLiveActivity] else { deactivateReminder() return } - let leadMinutes = Defaults[.reminderLeadTime] - let upcoming = latestEvents - .filter { !shouldHide($0) } - .compactMap { makeEntry(from: $0, leadMinutes: leadMinutes, referenceDate: referenceDate) } - .sorted { $0.triggerDate < $1.triggerDate } - upcomingEntries = upcoming updateActiveWindowReminders(for: referenceDate) @@ -174,6 +266,34 @@ final class ReminderLiveActivityManager: ObservableObject { handleEntrySelection(first, referenceDate: referenceDate) } + nonisolated private static func buildUpcomingEntries( + events: [EventModel], + leadMinutes: Int, + referenceDate: Date, + hideAllDayEvents: Bool, + hideCompletedReminders: Bool + ) -> [ReminderEntry] { + guard !Task.isCancelled else { return [] } + let leadSeconds = max(1, leadMinutes) * 60 + var entries: [ReminderEntry] = [] + entries.reserveCapacity(events.count) + for event in events { + if Task.isCancelled { return [] } + if hideAllDayEvents && event.isAllDay { + continue + } + if hideCompletedReminders, + case let .reminder(completed) = event.type, + completed { + continue + } + guard event.start > referenceDate else { continue } + let trigger = event.start.addingTimeInterval(TimeInterval(-leadSeconds)) + entries.append(.init(event: event, triggerDate: trigger, leadTime: TimeInterval(leadSeconds))) + } + return entries.sorted { $0.triggerDate < $1.triggerDate } + } + private func clearActiveReminderState() { nextReminder = nil if activeReminder != nil {