TheNoiseClock/TheNoiseClock/Features/Alarms/State/AlarmViewModel.swift

158 lines
4.8 KiB
Swift

//
// AlarmViewModel.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import AlarmKit
import Bedrock
import Foundation
import Observation
/// ViewModel for alarm management using AlarmKit (iOS 26+).
/// AlarmKit provides alarms that cut through Focus modes and silent mode,
/// with built-in Live Activity countdown and system alarm UI.
@Observable
class AlarmViewModel {
// MARK: - Properties
private let alarmService: AlarmService
private let alarmKitService = AlarmKitService.shared
/// Whether AlarmKit is authorized
var isAlarmKitAuthorized: Bool {
alarmKitService.authorizationState == .authorized
}
var alarms: [Alarm] {
alarmService.alarms
}
var systemSounds: [String] {
AppConstants.SystemSounds.availableSounds
}
// MARK: - Initialization
init(alarmService: AlarmService = AlarmService()) {
self.alarmService = alarmService
}
// MARK: - Authorization
/// Request AlarmKit authorization. Should be called during onboarding.
func requestAlarmKitAuthorization() async -> Bool {
return await alarmKitService.requestAuthorization()
}
// MARK: - Alarm CRUD Operations
func addAlarm(_ alarm: Alarm) async {
alarmService.addAlarm(alarm)
// Schedule with AlarmKit if alarm is enabled
if alarm.isEnabled {
Design.debugLog("[alarms] Scheduling AlarmKit alarm for \(alarm.label)")
do {
try await alarmKitService.scheduleAlarm(alarm)
} catch {
Design.debugLog("[alarms] AlarmKit scheduling failed: \(error)")
}
}
}
func updateAlarm(_ alarm: Alarm) async {
alarmService.updateAlarm(alarm)
// Cancel existing and reschedule if enabled
alarmKitService.cancelAlarm(id: alarm.id)
if alarm.isEnabled {
Design.debugLog("[alarms] Rescheduling AlarmKit alarm for \(alarm.label)")
do {
try await alarmKitService.scheduleAlarm(alarm)
} catch {
Design.debugLog("[alarms] AlarmKit rescheduling failed: \(error)")
}
}
}
func deleteAlarm(id: UUID) async {
// Cancel AlarmKit alarm first
alarmKitService.cancelAlarm(id: id)
// Then delete from storage
alarmService.deleteAlarm(id: id)
}
func toggleAlarm(id: UUID) async {
guard var alarm = alarmService.getAlarm(id: id) else { return }
alarm.isEnabled.toggle()
alarmService.updateAlarm(alarm)
// Schedule or cancel based on new state
if alarm.isEnabled {
Design.debugLog("[alarms] Enabling AlarmKit alarm \(alarm.label)")
do {
try await alarmKitService.scheduleAlarm(alarm)
} catch {
Design.debugLog("[alarms] AlarmKit scheduling failed: \(error)")
}
} else {
alarmKitService.cancelAlarm(id: id)
}
}
func getAlarm(id: UUID) -> Alarm? {
return alarmService.getAlarm(id: id)
}
func createNewAlarm(
time: Date,
soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false,
volume: Float = 1.0
) -> Alarm {
return Alarm(
id: UUID(),
time: time,
isEnabled: true,
soundName: soundName,
label: label,
notificationMessage: notificationMessage,
snoozeDuration: snoozeDuration,
isVibrationEnabled: isVibrationEnabled,
isLightFlashEnabled: isLightFlashEnabled,
volume: volume
)
}
// MARK: - App Lifecycle
/// Reschedule all enabled alarms with AlarmKit.
/// Call this on app launch to ensure alarms are registered.
func rescheduleAllAlarms() async {
Design.debugLog("[alarmkit] ========== RESCHEDULING ALL ALARMS ==========")
let enabledAlarms = alarmService.getEnabledAlarms()
Design.debugLog("[alarmkit] Found \(enabledAlarms.count) enabled alarm(s)")
for alarm in enabledAlarms {
Design.debugLog("[alarmkit] Scheduling: \(alarm.label) at \(alarm.time)")
do {
try await alarmKitService.scheduleAlarm(alarm)
} catch {
Design.debugLog("[alarmkit] ❌ Failed to reschedule \(alarm.label): \(error)")
}
}
Design.debugLog("[alarmkit] ========== RESCHEDULING COMPLETE ==========")
alarmKitService.logCurrentAlarms()
}
}