147 lines
5.1 KiB
Swift
147 lines
5.1 KiB
Swift
//
|
|
// AlarmService.swift
|
|
// TheNoiseClock
|
|
//
|
|
// Created by Matt Bruce on 9/7/25.
|
|
//
|
|
// NOTE: This service now only handles alarm persistence.
|
|
// Alarm scheduling is handled by AlarmKitService (iOS 26+).
|
|
// The old notification scheduling code has been removed.
|
|
//
|
|
|
|
import Foundation
|
|
import Observation
|
|
import Bedrock
|
|
|
|
/// Service for managing alarm persistence.
|
|
/// Alarm scheduling is handled by AlarmKitService.
|
|
@Observable
|
|
class AlarmService {
|
|
|
|
// MARK: - Singleton
|
|
static let shared = AlarmService()
|
|
|
|
// MARK: - Properties
|
|
private(set) var alarms: [Alarm] = []
|
|
private var alarmLookup: [UUID: Int] = [:]
|
|
private var persistenceWorkItem: DispatchWorkItem?
|
|
|
|
// MARK: - Initialization
|
|
init() {
|
|
loadAlarms()
|
|
Design.debugLog("[alarms] AlarmService initialized with \(alarms.count) alarms")
|
|
}
|
|
|
|
// MARK: - Public Interface
|
|
|
|
/// Add an alarm to storage. Does NOT schedule - caller should use AlarmKitService.
|
|
func addAlarm(_ alarm: Alarm) {
|
|
Design.debugLog("[alarms] AlarmService.addAlarm: \(alarm.label) at \(alarm.time)")
|
|
alarms.append(alarm)
|
|
updateAlarmLookup()
|
|
saveAlarms()
|
|
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
|
}
|
|
|
|
/// Update an alarm in storage. Does NOT reschedule - caller should use AlarmKitService.
|
|
func updateAlarm(_ alarm: Alarm) {
|
|
guard let index = alarmLookup[alarm.id] else {
|
|
Design.debugLog("[alarms] AlarmService.updateAlarm: alarm not found \(alarm.id)")
|
|
return
|
|
}
|
|
Design.debugLog("[alarms] AlarmService.updateAlarm: \(alarm.label) enabled=\(alarm.isEnabled)")
|
|
alarms[index] = alarm
|
|
updateAlarmLookup()
|
|
saveAlarms()
|
|
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
|
}
|
|
|
|
/// Delete an alarm from storage. Does NOT cancel - caller should use AlarmKitService.
|
|
func deleteAlarm(id: UUID) {
|
|
Design.debugLog("[alarms] AlarmService.deleteAlarm: \(id)")
|
|
alarms.removeAll { $0.id == id }
|
|
updateAlarmLookup()
|
|
saveAlarms()
|
|
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
|
}
|
|
|
|
/// Toggle an alarm's enabled state. Does NOT reschedule - caller should use AlarmKitService.
|
|
func toggleAlarm(id: UUID) {
|
|
guard let index = alarmLookup[id] else { return }
|
|
alarms[index].isEnabled.toggle()
|
|
Design.debugLog("[alarms] AlarmService.toggleAlarm: \(id) now enabled=\(alarms[index].isEnabled)")
|
|
saveAlarms()
|
|
NotificationCenter.default.post(name: .clockStyleDidUpdate, object: nil)
|
|
}
|
|
|
|
func getAlarm(id: UUID) -> Alarm? {
|
|
return alarms.first { $0.id == id }
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
private func updateAlarmLookup() {
|
|
alarmLookup.removeAll()
|
|
for (index, alarm) in alarms.enumerated() {
|
|
alarmLookup[alarm.id] = index
|
|
}
|
|
}
|
|
|
|
private func saveAlarms() {
|
|
persistenceWorkItem?.cancel()
|
|
|
|
let alarmsSnapshot = self.alarms
|
|
let work = DispatchWorkItem {
|
|
if let encoded = try? JSONEncoder().encode(alarmsSnapshot) {
|
|
UserDefaults.standard.set(encoded, forKey: AppConstants.StorageKeys.savedAlarms)
|
|
}
|
|
}
|
|
persistenceWorkItem = work
|
|
|
|
DispatchQueue.main.asyncAfter(
|
|
deadline: .now() + AppConstants.PersistenceDelays.alarms,
|
|
execute: work
|
|
)
|
|
}
|
|
|
|
private func loadAlarms() {
|
|
if let savedAlarms = UserDefaults.standard.data(forKey: AppConstants.StorageKeys.savedAlarms),
|
|
let decodedAlarms = try? JSONDecoder().decode([Alarm].self, from: savedAlarms) {
|
|
// Migrate sound file extensions from .caf to .mp3
|
|
alarms = decodedAlarms.map { alarm in
|
|
var migratedAlarm = alarm
|
|
migratedAlarm.soundName = migrateSoundName(alarm.soundName)
|
|
return migratedAlarm
|
|
}
|
|
updateAlarmLookup()
|
|
Design.debugLog("[alarms] Loaded \(alarms.count) alarms from storage")
|
|
|
|
// Save migrated alarms if any changes were made
|
|
let needsMigration = zip(decodedAlarms, alarms).contains { $0.soundName != $1.soundName }
|
|
if needsMigration {
|
|
Design.debugLog("[alarms] Sound file migration applied, saving...")
|
|
saveAlarms()
|
|
}
|
|
// Note: AlarmKit scheduling is handled by AlarmViewModel.rescheduleAllAlarms()
|
|
}
|
|
}
|
|
|
|
/// Migrate sound file names from .caf to .mp3
|
|
private func migrateSoundName(_ soundName: String) -> String {
|
|
if soundName.hasSuffix(".caf") {
|
|
let migrated = soundName.replacingOccurrences(of: ".caf", with: ".mp3")
|
|
Design.debugLog("[alarms] Migrating sound: \(soundName) -> \(migrated)")
|
|
return migrated
|
|
}
|
|
return soundName
|
|
}
|
|
|
|
/// Get all enabled alarms (for rescheduling with AlarmKit)
|
|
var enabledAlarms: [Alarm] {
|
|
return alarms.filter { $0.isEnabled }
|
|
}
|
|
|
|
func getEnabledAlarms() -> [Alarm] {
|
|
return enabledAlarms
|
|
}
|
|
}
|