learning_ai_clock/ios/ChronoMind/Shared/Notifications/NotificationScheduler.swift

186 lines
6.0 KiB
Swift

// Notification Scheduling
// UNUserNotificationCenter wrapper for pre-warning cascade + timer fire
// iOS-native notification system
import Foundation
import UserNotifications
// MARK: - Notification Manager
@MainActor
final class CMNotificationManager: ObservableObject {
static let shared = CMNotificationManager()
@Published var isAuthorized = false
private init() {}
// MARK: - Permission
func requestPermission() async {
do {
let granted = try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge, .criticalAlert])
isAuthorized = granted
} catch {
isAuthorized = false
}
}
func checkPermission() async {
let settings = await UNUserNotificationCenter.current().notificationSettings()
isAuthorized = settings.authorizationStatus == .authorized
}
// MARK: - Schedule Timer Notifications
/// Schedule all cascade warnings + final fire notification for a timer
func scheduleNotifications(for timer: CMTimer) {
// Remove existing notifications for this timer
removeNotifications(for: timer.id)
// Schedule cascade warnings
for warning in timer.warnings where !warning.fired {
scheduleWarning(timer: timer, warning: warning)
}
// Schedule fire notification
scheduleFireNotification(timer: timer)
}
/// Remove all scheduled notifications for a timer
func removeNotifications(for timerId: String) {
let center = UNUserNotificationCenter.current()
// Remove all notifications with this timer's ID prefix
center.getPendingNotificationRequests { requests in
let ids = requests
.filter { $0.identifier.hasPrefix("cm-\(timerId)") }
.map { $0.identifier }
center.removePendingNotificationRequests(withIdentifiers: ids)
}
}
/// Remove all ChronoMind notifications
func removeAllNotifications() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
// MARK: - Private
private func scheduleWarning(timer: CMTimer, warning: CascadeWarning) {
let content = UNMutableNotificationContent()
let timeStr = formatMinutesBefore(warning.minutesBefore)
content.title = "\(timer.label) in \(timeStr)"
content.body = "Pre-warning: \"\(timer.label)\" fires in \(timeStr)"
content.sound = soundForUrgency(timer.urgency, isWarning: true)
content.categoryIdentifier = "TIMER_WARNING"
content.userInfo = [
"timerId": timer.id,
"warningId": warning.id,
"type": "warning",
]
// Use the warning's scheduled time
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: max(1, warning.scheduledTime.timeIntervalSinceNow),
repeats: false
)
let request = UNNotificationRequest(
identifier: "cm-\(timer.id)-warning-\(warning.id)",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request)
}
private func scheduleFireNotification(timer: CMTimer) {
let content = UNMutableNotificationContent()
content.title = "🔔 \(timer.label) — NOW!"
content.body = "Timer \"\(timer.label)\" is firing!"
content.sound = soundForUrgency(timer.urgency, isWarning: false)
content.categoryIdentifier = "TIMER_FIRE"
content.userInfo = [
"timerId": timer.id,
"type": "fire",
]
// Interrupt level for important/critical
if #available(iOS 15.0, *) {
switch timer.urgency {
case .critical:
content.interruptionLevel = .critical
case .important:
content.interruptionLevel = .timeSensitive
case .standard:
content.interruptionLevel = .active
case .gentle:
content.interruptionLevel = .passive
case .passive:
content.interruptionLevel = .passive
}
}
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: max(1, timer.targetTime.timeIntervalSinceNow),
repeats: false
)
let request = UNNotificationRequest(
identifier: "cm-\(timer.id)-fire",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request)
}
private func soundForUrgency(_ urgency: UrgencyLevel, isWarning: Bool) -> UNNotificationSound? {
let config = getUrgencyConfig(urgency)
guard config.soundEnabled else { return nil }
if urgency == .critical && !isWarning {
return .defaultCritical
}
return .default
}
// MARK: - Notification Categories
func registerCategories() {
let snooze5 = UNNotificationAction(
identifier: "SNOOZE_5",
title: "Snooze 5m",
options: []
)
let snooze15 = UNNotificationAction(
identifier: "SNOOZE_15",
title: "Snooze 15m",
options: []
)
let dismiss = UNNotificationAction(
identifier: "DISMISS",
title: "Dismiss",
options: [.destructive]
)
let fireCategory = UNNotificationCategory(
identifier: "TIMER_FIRE",
actions: [snooze5, snooze15, dismiss],
intentIdentifiers: [],
options: []
)
let warningCategory = UNNotificationCategory(
identifier: "TIMER_WARNING",
actions: [dismiss],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([fireCategory, warningCategory])
}
}