// // AlarmKitService.swift // TheNoiseClock // // Created by Matt Bruce on 2/2/26. // import AlarmKit import Bedrock import Foundation import SwiftUI /// Service for managing alarms using AlarmKit (iOS 26+). /// AlarmKit alarms cut through Focus modes and silent mode. @MainActor final class AlarmKitService { // MARK: - Singleton static let shared = AlarmKitService() private let manager = AlarmManager.shared private init() { Design.debugLog("[alarmkit] AlarmKitService initialized") Design.debugLog("[alarmkit] Authorization state: \(manager.authorizationState)") } // MARK: - Authorization /// The current authorization state for AlarmKit var authorizationState: AlarmManager.AuthorizationState { manager.authorizationState } /// Request authorization to schedule alarms. /// - Returns: `true` if authorized, `false` otherwise. func requestAuthorization() async -> Bool { Design.debugLog("[alarmkit] Requesting authorization, current state: \(manager.authorizationState)") switch manager.authorizationState { case .notDetermined: do { let state = try await manager.requestAuthorization() Design.debugLog("[alarmkit] Authorization result: \(state)") return state == .authorized } catch { Design.debugLog("[alarmkit] Authorization error: \(error)") return false } case .authorized: Design.debugLog("[alarmkit] Already authorized") return true case .denied: Design.debugLog("[alarmkit] Authorization denied - user must enable in Settings") return false @unknown default: Design.debugLog("[alarmkit] Unknown authorization state") return false } } // MARK: - Scheduling /// Schedule an alarm using AlarmKit. /// - Parameter alarm: The alarm to schedule. func scheduleAlarm(_ alarm: Alarm) async throws { Design.debugLog("[alarmkit] ========== SCHEDULING ALARM ==========") Design.debugLog("[alarmkit] Label: \(alarm.label)") Design.debugLog("[alarmkit] Time: \(alarm.time)") Design.debugLog("[alarmkit] Sound: \(alarm.soundName)") Design.debugLog("[alarmkit] Volume: \(alarm.volume)") Design.debugLog("[alarmkit] ID: \(alarm.id)") // Ensure we're authorized if manager.authorizationState != .authorized { Design.debugLog("[alarmkit] Not authorized, requesting...") let authorized = await requestAuthorization() guard authorized else { Design.debugLog("[alarmkit] Authorization failed, cannot schedule alarm") throw AlarmKitError.notAuthorized } } // Create the sound for the alarm let alarmSound = createAlarmSound(for: alarm) Design.debugLog("[alarmkit] Created alarm sound: \(alarmSound)") // Create the alert presentation with stop button and sound let stopButton = AlarmButton( text: "Stop", textColor: .red, systemImageName: "stop.fill" ) let snoozeButton = AlarmButton( text: "Snooze", textColor: .blue, systemImageName: "zzz" ) let alert = AlarmPresentation.Alert( title: LocalizedStringResource(stringLiteral: alarm.label), sound: alarmSound, stopButton: stopButton, snoozeButton: snoozeButton ) Design.debugLog("[alarmkit] Created alert with sound and buttons") // Create metadata for the alarm let metadata = NoiseClockAlarmMetadata( alarmId: alarm.id.uuidString, soundName: alarm.soundName, snoozeDuration: alarm.snoozeDuration, label: alarm.label, volume: alarm.volume ) Design.debugLog("[alarmkit] Created metadata: \(metadata)") // Create alarm attributes let attributes = AlarmAttributes( presentation: AlarmPresentation(alert: alert), metadata: metadata, tintColor: Color.pink ) Design.debugLog("[alarmkit] Created attributes with tint color") // Create the schedule let schedule = createSchedule(for: alarm) Design.debugLog("[alarmkit] Created schedule: \(schedule)") // Create countdown duration (5 min before alarm, 1 min after) let countdownDuration = AlarmKit.Alarm.CountdownDuration( preAlert: 300, // 5 minutes before postAlert: 60 // 1 minute after ) Design.debugLog("[alarmkit] Countdown duration: preAlert=300s, postAlert=60s") // Create the alarm configuration let configuration = AlarmManager.AlarmConfiguration( countdownDuration: countdownDuration, schedule: schedule, attributes: attributes ) Design.debugLog("[alarmkit] Created configuration") // Schedule the alarm do { let scheduledAlarm = try await manager.schedule( id: alarm.id, configuration: configuration ) Design.debugLog("[alarmkit] ✅ ALARM SCHEDULED SUCCESSFULLY") Design.debugLog("[alarmkit] Scheduled ID: \(scheduledAlarm.id)") Design.debugLog("[alarmkit] Scheduled state: \(scheduledAlarm.state)") } catch { Design.debugLog("[alarmkit] ❌ SCHEDULING FAILED: \(error)") throw AlarmKitError.schedulingFailed(error) } } // MARK: - Sound Configuration /// Create an AlarmKit sound from the alarm's sound name. private func createAlarmSound(for alarm: Alarm) -> AlarmKit.AlertSound { let soundName = alarm.soundName Design.debugLog("[alarmkit] Creating sound for: \(soundName)") // Check if it's a bundled sound file (has extension) if soundName.contains(".") { // Extract filename without extension for named sound let soundFileName = soundName Design.debugLog("[alarmkit] Using named sound file: \(soundFileName)") return .named(soundFileName) } else { // Assume it's a named sound resource Design.debugLog("[alarmkit] Using named sound: \(soundName)") return .named(soundName) } } /// Cancel a scheduled alarm. /// - Parameter id: The UUID of the alarm to cancel. func cancelAlarm(id: UUID) { Design.debugLog("[alarmkit] Cancelling alarm: \(id)") do { try manager.cancel(id: id) Design.debugLog("[alarmkit] ✅ Alarm cancelled: \(id)") } catch { Design.debugLog("[alarmkit] ❌ Cancel error: \(error)") } } /// Stop an active alarm that is currently alerting. /// - Parameter id: The UUID of the alarm to stop. func stopAlarm(id: UUID) { Design.debugLog("[alarmkit] Stopping alarm: \(id)") do { try manager.stop(id: id) Design.debugLog("[alarmkit] ✅ Alarm stopped: \(id)") } catch { Design.debugLog("[alarmkit] ❌ Stop error: \(error)") } } /// Snooze an active alarm by starting its countdown again. /// - Parameter id: The UUID of the alarm to snooze. func snoozeAlarm(id: UUID) { Design.debugLog("[alarmkit] Snoozing alarm: \(id)") do { try manager.countdown(id: id) Design.debugLog("[alarmkit] ✅ Alarm snoozed: \(id)") } catch { Design.debugLog("[alarmkit] ❌ Snooze error: \(error)") } } // MARK: - Alarm Updates /// Async sequence that emits the current set of alarms whenever changes occur. var alarmUpdates: some AsyncSequence<[AlarmKit.Alarm], Never> { manager.alarmUpdates } /// Log current state of all scheduled alarms func logCurrentAlarms() { Design.debugLog("[alarmkit] ========== CURRENT ALARMS ==========") Task { for await alarms in manager.alarmUpdates { Design.debugLog("[alarmkit] Found \(alarms.count) alarm(s) in AlarmKit") for alarm in alarms { Design.debugLog("[alarmkit] - ID: \(alarm.id)") Design.debugLog("[alarmkit] State: \(alarm.state)") } break // Just log once } } } // MARK: - Private Methods /// Create an AlarmKit schedule from an Alarm model. private func createSchedule(for alarm: Alarm) -> AlarmKit.Alarm.Schedule { // Extract time components let calendar = Calendar.current let components = calendar.dateComponents([.hour, .minute], from: alarm.time) let hour = components.hour ?? 0 let minute = components.minute ?? 0 Design.debugLog("[alarmkit] Creating schedule for \(hour):\(String(format: "%02d", minute))") let time = AlarmKit.Alarm.Schedule.Relative.Time( hour: hour, minute: minute ) // For now, create a one-time alarm (non-repeating) // Future: Support weekly repeating alarms based on alarm.repeatDays let schedule = AlarmKit.Alarm.Schedule.relative( AlarmKit.Alarm.Schedule.Relative( time: time, repeats: .never ) ) Design.debugLog("[alarmkit] Schedule created: relative, repeats=never") return schedule } } // MARK: - Errors enum AlarmKitError: Error, LocalizedError { case notAuthorized case schedulingFailed(Error) var errorDescription: String? { switch self { case .notAuthorized: return "AlarmKit is not authorized. Please enable alarm permissions in Settings." case .schedulingFailed(let error): return "Failed to schedule alarm: \(error.localizedDescription)" } } }