From f4f365f4c7b9d1cef1ee5684f4fbf0653c6be9bc Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 8 Sep 2025 15:34:47 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Core/Utilities/NotificationUtils.swift | 5 +- TheNoiseClock/Models/Alarm.swift | 3 + TheNoiseClock/Services/AlarmService.swift | 4 +- TheNoiseClock/Services/FocusModeService.swift | 10 +- TheNoiseClock/ViewModels/AlarmViewModel.swift | 8 +- TheNoiseClock/Views/Alarms/AddAlarmView.swift | 104 ++++++----- TheNoiseClock/Views/Alarms/AlarmView.swift | 11 +- .../NotificationMessageEditView.swift | 55 ++++++ .../Views/Alarms/EditAlarmView.swift | 165 ++++++++++++++++++ 9 files changed, 312 insertions(+), 53 deletions(-) create mode 100644 TheNoiseClock/Views/Alarms/Components/NotificationMessageEditView.swift create mode 100644 TheNoiseClock/Views/Alarms/EditAlarmView.swift diff --git a/TheNoiseClock/Core/Utilities/NotificationUtils.swift b/TheNoiseClock/Core/Utilities/NotificationUtils.swift index 98ca0dc..1f42a9b 100644 --- a/TheNoiseClock/Core/Utilities/NotificationUtils.swift +++ b/TheNoiseClock/Core/Utilities/NotificationUtils.swift @@ -39,7 +39,10 @@ enum NotificationUtils { if soundName == AppConstants.SystemSounds.defaultSound { content.sound = UNNotificationSound.default } else { - content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "\(soundName).caf")) + // For alarm sounds, use the default notification sound since custom sounds need to be in the app bundle + // and properly configured. For now, use default to ensure notifications work. + content.sound = UNNotificationSound.default + print("🔔 Using default notification sound for alarm: \(soundName)") } return content diff --git a/TheNoiseClock/Models/Alarm.swift b/TheNoiseClock/Models/Alarm.swift index 6dc54e8..b18be21 100644 --- a/TheNoiseClock/Models/Alarm.swift +++ b/TheNoiseClock/Models/Alarm.swift @@ -14,6 +14,7 @@ struct Alarm: Identifiable, Codable, Equatable { var isEnabled: Bool var soundName: String var label: String + var notificationMessage: String // Custom notification message var snoozeDuration: Int // in minutes var isVibrationEnabled: Bool var isLightFlashEnabled: Bool @@ -26,6 +27,7 @@ struct Alarm: Identifiable, Codable, Equatable { isEnabled: Bool = true, soundName: String = AppConstants.SystemSounds.defaultSound, label: String = "Alarm", + notificationMessage: String = "Your alarm is ringing", snoozeDuration: Int = 9, isVibrationEnabled: Bool = true, isLightFlashEnabled: Bool = false, @@ -36,6 +38,7 @@ struct Alarm: Identifiable, Codable, Equatable { self.isEnabled = isEnabled self.soundName = soundName self.label = label + self.notificationMessage = notificationMessage self.snoozeDuration = snoozeDuration self.isVibrationEnabled = isVibrationEnabled self.isLightFlashEnabled = isLightFlashEnabled diff --git a/TheNoiseClock/Services/AlarmService.swift b/TheNoiseClock/Services/AlarmService.swift index c3c6ced..0db925e 100644 --- a/TheNoiseClock/Services/AlarmService.swift +++ b/TheNoiseClock/Services/AlarmService.swift @@ -84,8 +84,8 @@ class AlarmService { // Use FocusModeService for better Focus mode compatibility focusModeService.scheduleAlarmNotification( identifier: alarm.id.uuidString, - title: "Wake Up!", - body: "Your alarm is ringing.", + title: alarm.label, + body: alarm.notificationMessage, date: alarm.time, soundName: alarm.soundName, repeats: false // For now, set to false since Alarm model doesn't have repeatDays diff --git a/TheNoiseClock/Services/FocusModeService.swift b/TheNoiseClock/Services/FocusModeService.swift index 7bf518d..6b127d9 100644 --- a/TheNoiseClock/Services/FocusModeService.swift +++ b/TheNoiseClock/Services/FocusModeService.swift @@ -100,7 +100,10 @@ class FocusModeService { let content = UNMutableNotificationContent() content.title = title content.body = body - content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName)) + // Use default notification sound for now to ensure notifications work + // Custom sounds require proper bundle configuration + content.sound = UNNotificationSound.default + print("🔔 Using default notification sound for alarm: \(soundName)") content.categoryIdentifier = "ALARM_CATEGORY" content.userInfo = [ "alarmId": identifier, @@ -115,7 +118,10 @@ class FocusModeService { let components = calendar.dateComponents([.hour, .minute], from: date) trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) } else { - trigger = UNTimeIntervalNotificationTrigger(timeInterval: date.timeIntervalSinceNow, repeats: false) + // Use calendar trigger for one-time alarms to avoid time interval issues + let calendar = Calendar.current + let components = calendar.dateComponents([.hour, .minute], from: date) + trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) } // Create request diff --git a/TheNoiseClock/ViewModels/AlarmViewModel.swift b/TheNoiseClock/ViewModels/AlarmViewModel.swift index 86c1eaa..c0c6152 100644 --- a/TheNoiseClock/ViewModels/AlarmViewModel.swift +++ b/TheNoiseClock/ViewModels/AlarmViewModel.swift @@ -40,7 +40,7 @@ class AlarmViewModel { await notificationService.scheduleAlarmNotification( id: alarm.id.uuidString, title: alarm.label, - body: "Time to wake up!", + body: alarm.notificationMessage, soundName: alarm.soundName, date: alarm.time ) @@ -55,7 +55,7 @@ class AlarmViewModel { await notificationService.scheduleAlarmNotification( id: alarm.id.uuidString, title: alarm.label, - body: "Time to wake up!", + body: alarm.notificationMessage, soundName: alarm.soundName, date: alarm.time ) @@ -83,7 +83,7 @@ class AlarmViewModel { await notificationService.scheduleAlarmNotification( id: alarm.id.uuidString, title: alarm.label, - body: "Time to wake up!", + body: alarm.notificationMessage, soundName: alarm.soundName, date: alarm.time ) @@ -100,6 +100,7 @@ class AlarmViewModel { 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, @@ -111,6 +112,7 @@ class AlarmViewModel { isEnabled: true, soundName: soundName, label: label, + notificationMessage: notificationMessage, snoozeDuration: snoozeDuration, isVibrationEnabled: isVibrationEnabled, isLightFlashEnabled: isLightFlashEnabled, diff --git a/TheNoiseClock/Views/Alarms/AddAlarmView.swift b/TheNoiseClock/Views/Alarms/AddAlarmView.swift index 9784ebc..680c668 100644 --- a/TheNoiseClock/Views/Alarms/AddAlarmView.swift +++ b/TheNoiseClock/Views/Alarms/AddAlarmView.swift @@ -17,6 +17,7 @@ struct AddAlarmView: View { @State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date() @State private var selectedSoundName = "digital-alarm.mp3" @State private var alarmLabel = "Alarm" + @State private var notificationMessage = "Your alarm is ringing" @State private var snoozeDuration = 9 // minutes @State private var isVibrationEnabled = true @State private var isLightFlashEnabled = false @@ -24,53 +25,67 @@ struct AddAlarmView: View { var body: some View { NavigationView { - ScrollView { - VStack(spacing: 0) { - TimePickerSection(selectedTime: $newAlarmTime) - TimeUntilAlarmSection(alarmTime: newAlarmTime) - - List { - // Label Section - NavigationLink(destination: LabelEditView(label: $alarmLabel)) { - HStack { - Image(systemName: "textformat") - .foregroundColor(UIConstants.Colors.accentColor) - .frame(width: 24) - Text("Label") - Spacer() - Text(alarmLabel) - .foregroundColor(.secondary) - } - } - - // Sound Section - NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { - HStack { - Image(systemName: "music.note") - .foregroundColor(UIConstants.Colors.accentColor) - .frame(width: 24) - Text("Sound") - Spacer() - Text(getSoundDisplayName(selectedSoundName)) - .foregroundColor(.secondary) - } - } - - // Snooze Section - NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { - HStack { - Image(systemName: "clock.arrow.circlepath") - .foregroundColor(UIConstants.Colors.accentColor) - .frame(width: 24) - Text("Snooze") - Spacer() - Text("for \(snoozeDuration) min") - .foregroundColor(.secondary) - } + VStack(spacing: 0) { + // Time picker section at top + TimePickerSection(selectedTime: $newAlarmTime) + TimeUntilAlarmSection(alarmTime: newAlarmTime) + + // List for settings below + List { + // Label Section + NavigationLink(destination: LabelEditView(label: $alarmLabel)) { + HStack { + Image(systemName: "textformat") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Label") + Spacer() + Text(alarmLabel) + .foregroundColor(.secondary) + } + } + + // Notification Message Section + NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) { + HStack { + Image(systemName: "message") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Message") + Spacer() + Text(notificationMessage) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + + // Sound Section + NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { + HStack { + Image(systemName: "music.note") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Sound") + Spacer() + Text(getSoundDisplayName(selectedSoundName)) + .foregroundColor(.secondary) + } + } + + // Snooze Section + NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { + HStack { + Image(systemName: "clock.arrow.circlepath") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Snooze") + Spacer() + Text("for \(snoozeDuration) min") + .foregroundColor(.secondary) } } - .listStyle(.insetGrouped) } + .listStyle(.insetGrouped) } .navigationTitle("Alarm") .navigationBarTitleDisplayMode(.inline) @@ -90,6 +105,7 @@ struct AddAlarmView: View { time: newAlarmTime, soundName: selectedSoundName, label: alarmLabel, + notificationMessage: notificationMessage, snoozeDuration: snoozeDuration, isVibrationEnabled: isVibrationEnabled, isLightFlashEnabled: isLightFlashEnabled, diff --git a/TheNoiseClock/Views/Alarms/AlarmView.swift b/TheNoiseClock/Views/Alarms/AlarmView.swift index b113d8a..7ed61c8 100644 --- a/TheNoiseClock/Views/Alarms/AlarmView.swift +++ b/TheNoiseClock/Views/Alarms/AlarmView.swift @@ -13,6 +13,7 @@ struct AlarmView: View { // MARK: - Properties @State private var viewModel = AlarmViewModel() @State private var showAddAlarm = false + @State private var selectedAlarmForEdit: Alarm? // MARK: - Body var body: some View { @@ -35,7 +36,9 @@ struct AlarmView: View { await viewModel.toggleAlarm(id: alarm.id) } }, - onEdit: { /* TODO: Implement edit functionality */ } + onEdit: { + selectedAlarmForEdit = alarm + } ) } .onDelete(perform: deleteAlarm) @@ -64,6 +67,12 @@ struct AlarmView: View { isPresented: $showAddAlarm ) } + .sheet(item: $selectedAlarmForEdit) { alarm in + EditAlarmView( + viewModel: viewModel, + alarm: alarm + ) + } } // MARK: - Private Methods diff --git a/TheNoiseClock/Views/Alarms/Components/NotificationMessageEditView.swift b/TheNoiseClock/Views/Alarms/Components/NotificationMessageEditView.swift new file mode 100644 index 0000000..b68af2b --- /dev/null +++ b/TheNoiseClock/Views/Alarms/Components/NotificationMessageEditView.swift @@ -0,0 +1,55 @@ +// +// NotificationMessageEditView.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/7/25. +// + +import SwiftUI + +/// View for editing alarm notification message +struct NotificationMessageEditView: View { + @Binding var message: String + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: UIConstants.Spacing.large) { + TextField("Notification message", text: $message) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .contentPadding(horizontal: UIConstants.Spacing.large) + + // Preview section + VStack(alignment: .leading, spacing: UIConstants.Spacing.small) { + Text("Preview:") + .font(.headline) + .foregroundColor(.secondary) + + VStack(alignment: .leading, spacing: 4) { + Text("Alarm") + .font(.headline) + .foregroundColor(.primary) + + Text(message.isEmpty ? "Your alarm is ringing" : message) + .font(.body) + .foregroundColor(.secondary) + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(8) + } + .contentPadding(horizontal: UIConstants.Spacing.large) + + Spacer() + } + .navigationTitle("Message") + .navigationBarTitleDisplayMode(.inline) + .contentPadding(vertical: UIConstants.Spacing.large) + } +} + +// MARK: - Preview +#Preview { + NavigationStack { + NotificationMessageEditView(message: .constant("Your alarm is ringing")) + } +} diff --git a/TheNoiseClock/Views/Alarms/EditAlarmView.swift b/TheNoiseClock/Views/Alarms/EditAlarmView.swift new file mode 100644 index 0000000..0dc2856 --- /dev/null +++ b/TheNoiseClock/Views/Alarms/EditAlarmView.swift @@ -0,0 +1,165 @@ +// +// EditAlarmView.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/7/25. +// + +import SwiftUI + +/// View for editing existing alarms +struct EditAlarmView: View { + + // MARK: - Properties + let viewModel: AlarmViewModel + let alarm: Alarm + @Environment(\.dismiss) private var dismiss + + @State private var alarmTime: Date + @State private var selectedSoundName: String + @State private var alarmLabel: String + @State private var notificationMessage: String + @State private var snoozeDuration: Int + @State private var isVibrationEnabled: Bool + @State private var isLightFlashEnabled: Bool + @State private var volume: Float + + // MARK: - Initialization + init(viewModel: AlarmViewModel, alarm: Alarm) { + self.viewModel = viewModel + self.alarm = alarm + + // Initialize state with current alarm values + self._alarmTime = State(initialValue: alarm.time) + self._selectedSoundName = State(initialValue: alarm.soundName) + self._alarmLabel = State(initialValue: alarm.label) + self._notificationMessage = State(initialValue: alarm.notificationMessage) + self._snoozeDuration = State(initialValue: alarm.snoozeDuration) + self._isVibrationEnabled = State(initialValue: alarm.isVibrationEnabled) + self._isLightFlashEnabled = State(initialValue: alarm.isLightFlashEnabled) + self._volume = State(initialValue: alarm.volume) + } + + // MARK: - Body + var body: some View { + NavigationView { + VStack(spacing: 0) { + // Time picker section at top + TimePickerSection(selectedTime: $alarmTime) + TimeUntilAlarmSection(alarmTime: alarmTime) + + // List for settings below + List { + // Label Section + NavigationLink(destination: LabelEditView(label: $alarmLabel)) { + HStack { + Image(systemName: "textformat") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Label") + Spacer() + Text(alarmLabel) + .foregroundColor(.secondary) + } + } + + // Notification Message Section + NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) { + HStack { + Image(systemName: "message") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Message") + Spacer() + Text(notificationMessage) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + + // Sound Section + NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { + HStack { + Image(systemName: "music.note") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Sound") + Spacer() + Text(getSoundDisplayName(selectedSoundName)) + .foregroundColor(.secondary) + } + } + + // Snooze Section + NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { + HStack { + Image(systemName: "clock.arrow.circlepath") + .foregroundColor(UIConstants.Colors.accentColor) + .frame(width: 24) + Text("Snooze") + Spacer() + Text("for \(snoozeDuration) min") + .foregroundColor(.secondary) + } + } + } + .listStyle(.insetGrouped) + } + .navigationTitle("Edit Alarm") + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + .foregroundColor(UIConstants.Colors.accentColor) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + Task { + let updatedAlarm = Alarm( + id: alarm.id, // Keep the same ID + time: alarmTime, + isEnabled: alarm.isEnabled, // Keep the same enabled state + soundName: selectedSoundName, + label: alarmLabel, + notificationMessage: notificationMessage, + snoozeDuration: snoozeDuration, + isVibrationEnabled: isVibrationEnabled, + isLightFlashEnabled: isLightFlashEnabled, + volume: volume + ) + await viewModel.updateAlarm(updatedAlarm) + dismiss() + } + } + .foregroundColor(UIConstants.Colors.accentColor) + .fontWeight(.semibold) + } + } + } + } + + // MARK: - Helper Methods + private func getSoundDisplayName(_ fileName: String) -> String { + let alarmSounds = SoundConfigurationService.shared.getAlarmSounds() + if let sound = alarmSounds.first(where: { $0.fileName == fileName }) { + return sound.name + } + return fileName.replacingOccurrences(of: ".mp3", with: "").capitalized + } +} + +// MARK: - Preview +#Preview { + EditAlarmView( + viewModel: AlarmViewModel(), + alarm: Alarm( + time: Date(), + label: "Morning Alarm", + notificationMessage: "Time to wake up!" + ) + ) +}