TheNoiseClock/TheNoiseClock/Features/Alarms/Services/NotificationDelegate.swift
Matt Bruce a3398f0dd0 refactored with bedrock and organized folders
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-01-31 10:46:41 -06:00

180 lines
6.8 KiB
Swift

//
// NotificationDelegate.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/8/25.
//
import UserNotifications
import Foundation
/// Delegate to handle notification actions (snooze, stop, etc.)
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
// MARK: - Singleton
static let shared = NotificationDelegate()
// MARK: - Properties
private var alarmService: AlarmService?
// MARK: - Initialization
private override init() {
super.init()
setupNotificationCenter()
}
// MARK: - Setup
private func setupNotificationCenter() {
UNUserNotificationCenter.current().delegate = self
DebugLogger.log("Notification delegate configured", category: .settings)
}
/// Set the alarm service instance (called from AlarmViewModel)
func setAlarmService(_ service: AlarmService) {
self.alarmService = service
}
// MARK: - UNUserNotificationCenterDelegate
/// Handle notification actions when app is in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let actionIdentifier = response.actionIdentifier
let notification = response.notification
let userInfo = notification.request.content.userInfo
DebugLogger.log("Notification action received: \(actionIdentifier)", category: .settings)
switch actionIdentifier {
case "SNOOZE_ACTION":
handleSnoozeAction(userInfo: userInfo)
case "STOP_ACTION":
handleStopAction(userInfo: userInfo)
case UNNotificationDefaultActionIdentifier:
// User tapped the notification itself
handleNotificationTap(userInfo: userInfo)
default:
DebugLogger.log("Unknown action: \(actionIdentifier)", category: .settings)
}
completionHandler()
}
/// Handle notifications when app is in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Show notification even when app is in foreground
completionHandler([.banner, .sound, .badge])
}
// MARK: - Action Handlers
private func handleSnoozeAction(userInfo: [AnyHashable: Any]) {
guard let alarmIdString = userInfo["alarmId"] as? String,
let alarmId = UUID(uuidString: alarmIdString),
let alarmService = self.alarmService,
let alarm = alarmService.getAlarm(id: alarmId) else {
DebugLogger.log("Could not find alarm for snooze action", category: .general)
return
}
DebugLogger.log("Snoozing alarm: \(alarm.label) for \(alarm.snoozeDuration) minutes", category: .settings)
// Calculate snooze time (current time + snooze duration)
let snoozeTime = Date().addingTimeInterval(TimeInterval(alarm.snoozeDuration * 60))
DebugLogger.log("Snooze time: \(snoozeTime)", category: .settings)
DebugLogger.log("Current time: \(Date())", category: .settings)
// Create a temporary alarm for the snooze
let snoozeAlarm = Alarm(
id: UUID(), // New ID for snooze alarm
time: snoozeTime,
isEnabled: true,
soundName: alarm.soundName,
label: "\(alarm.label) (Snoozed)",
notificationMessage: "Snoozed: \(alarm.notificationMessage)",
snoozeDuration: alarm.snoozeDuration,
isVibrationEnabled: alarm.isVibrationEnabled,
isLightFlashEnabled: alarm.isLightFlashEnabled,
volume: alarm.volume
)
// Schedule the snooze notification
Task {
await scheduleSnoozeNotification(snoozeAlarm, userInfo: userInfo)
}
}
private func handleStopAction(userInfo: [AnyHashable: Any]) {
guard let alarmIdString = userInfo["alarmId"] as? String,
let alarmId = UUID(uuidString: alarmIdString) else {
DebugLogger.log("Could not find alarm ID for stop action", category: .general)
return
}
DebugLogger.log("Stopping alarm: \(alarmId)", category: .settings)
// Cancel any pending notifications for this alarm
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [alarmIdString])
// If this was a snooze alarm, we don't want to disable the original alarm
// Just cancel the current notification
}
private func handleNotificationTap(userInfo: [AnyHashable: Any]) {
guard let alarmIdString = userInfo["alarmId"] as? String,
let alarmId = UUID(uuidString: alarmIdString) else {
DebugLogger.log("Could not find alarm ID for notification tap", category: .general)
return
}
DebugLogger.log("Notification tapped for alarm: \(alarmId)", category: .settings)
// For now, just log the tap. In the future, this could open the alarm details
// or perform some other action when the user taps the notification
}
// MARK: - Private Methods
private func scheduleSnoozeNotification(_ snoozeAlarm: Alarm, userInfo: [AnyHashable: Any]) async {
let content = UNMutableNotificationContent()
content.title = snoozeAlarm.label
content.body = snoozeAlarm.notificationMessage
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: snoozeAlarm.soundName))
content.categoryIdentifier = "ALARM_CATEGORY"
content.userInfo = [
"alarmId": snoozeAlarm.id.uuidString,
"soundName": snoozeAlarm.soundName,
"isSnooze": true,
"originalAlarmId": userInfo["alarmId"] as? String ?? ""
]
// Create trigger for snooze time
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: snoozeAlarm.time.timeIntervalSinceNow,
repeats: false
)
// Create request
let request = UNNotificationRequest(
identifier: snoozeAlarm.id.uuidString,
content: content,
trigger: trigger
)
// Schedule notification
do {
try await UNUserNotificationCenter.current().add(request)
DebugLogger.log("Snooze notification scheduled for \(snoozeAlarm.time)", category: .settings)
} catch {
DebugLogger.log("Error scheduling snooze notification: \(error)", category: .general)
}
}
}