← Back to index

NotificationManager

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import UserNotifications import CombineFramework importsImports Foundation, UserNotifications, Combine.
class NotificationManager: ObservableObject { static let shared = NotificationManager() @Published var isAuthorized = false private init() { // Delay the authorization check until after init completes DispatchQueue.main.async { [weak self] in self?.checkAuthorizationStatus() } } // MARK: - Authorization func checkAuthorizationStatus() { UNUserNotificationCenter.current().getNotificationSettings { settings in DispatchQueue.main.async { self.isAuthorized = settings.authorizationStatus == .authorized } } } func requestAuthorization() async -> Bool { do { let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) await MainActor.run { self.isAuthorized = granted } return granted } catch { print("Error requesting notification authorization: \(error)") return false } } // MARK: - Scheduling Notifications func scheduleReminders(for entry: SpotEntry) { guard entry.remindersEnabled else { return } // First cancel any existing reminders for this entry cancelReminders(for: entry) let now = Date() for (index, interval) in SpotEntry.reminderIntervals.enumerated() { let reminderDate = entry.dateTime.addingTimeInterval(-interval.seconds) // Only schedule if the reminder time is in the future guard reminderDate > now else { continue } let content = UNMutableNotificationContent() content.title = "🎭 \(entry.playName)" content.body = reminderBodyText(intervalName: interval.name, cuisine: entry.cuisine) content.sound = .default content.userInfo = ["entryId": entry.id.uuidString] let triggerDate = Calendar.current.dateComponents( [.year, .month, .day, .hour, .minute], from: reminderDate ) let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false) let identifier = notificationIdentifier(for: entry, intervalIndex: index) let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print("Error scheduling notification: \(error)") } } } } func cancelReminders(for entry: SpotEntry) { let identifiers = SpotEntry.reminderIntervals.indices.map { index in notificationIdentifier(for: entry, intervalIndex: index) } UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers) } func cancelAllReminders(for entries: [SpotEntry]) { for entry in entries { cancelReminders(for: entry) } } /// Called at app launch. Fetches the current pending notifications once and /// only schedules reminders for entries that are missing them — avoids the /// O(n × 8) cancel+reschedule storm that previously happened on every cold start. func scheduleRemindersIfNeeded(for entries: [SpotEntry]) async { let existing = await UNUserNotificationCenter.current().pendingNotificationRequests() let existingIDs = Set(existing.map { $0.identifier }) for entry in entries where entry.remindersEnabled && entry.dateTime > Date() { // Check whether every expected reminder already exists let allPresent = SpotEntry.reminderIntervals.indices.allSatisfy { index in existingIDs.contains(notificationIdentifier(for: entry, intervalIndex: index)) } if !allPresent { scheduleReminders(for: entry) } } } // MARK: - Helpers private func notificationIdentifier(for entry: SpotEntry, intervalIndex: Int) -> String { return "entry-\(entry.id.uuidString)-reminder-\(intervalIndex)" } private func reminderBodyText(intervalName: String, cuisine: String) -> String { if cuisine.isEmpty { return "Starting in \(intervalName)!" } else { return "Starting in \(intervalName) at \(cuisine)!" } } // MARK: - Debug func printPendingNotifications() { UNUserNotificationCenter.current().getPendingNotificationRequests { requests in print("Pending notifications: \(requests.count)") for request in requests { print(" - \(request.identifier): \(request.content.title)") } } } }`NotificationManager` classDefines the `NotificationManager` class. Conforms to ObservableObject.