added features.
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
263b2fffcc
commit
8f79836481
3
PRD.md
3
PRD.md
@ -99,6 +99,7 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
|
|||||||
- **Sound preview**: Play/stop functionality for testing alarm sounds before selection
|
- **Sound preview**: Play/stop functionality for testing alarm sounds before selection
|
||||||
- **Custom labels**: User-defined alarm names and descriptions
|
- **Custom labels**: User-defined alarm names and descriptions
|
||||||
- **Repeat schedules**: Set alarms to repeat on specific weekdays or daily
|
- **Repeat schedules**: Set alarms to repeat on specific weekdays or daily
|
||||||
|
- **Locale-aware repeat picker**: Weekday ordering follows the user’s locale-first weekday
|
||||||
- **Sound selection**: Choose from extensive alarm sounds with live preview
|
- **Sound selection**: Choose from extensive alarm sounds with live preview
|
||||||
- **Volume control**: Adjustable alarm volume (0-100%)
|
- **Volume control**: Adjustable alarm volume (0-100%)
|
||||||
- **Vibration settings**: Enable/disable vibration for each alarm
|
- **Vibration settings**: Enable/disable vibration for each alarm
|
||||||
@ -110,6 +111,8 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
|
|||||||
- **AlarmKit authorization**: Requires user permission via NSAlarmKitUsageDescription
|
- **AlarmKit authorization**: Requires user permission via NSAlarmKitUsageDescription
|
||||||
- **Persistent storage**: Alarms saved to UserDefaults with backward compatibility
|
- **Persistent storage**: Alarms saved to UserDefaults with backward compatibility
|
||||||
- **Alarm management**: Add, edit, delete, and duplicate alarms
|
- **Alarm management**: Add, edit, delete, and duplicate alarms
|
||||||
|
- **Sorted alarm list**: Enabled alarms appear first and are ordered by nearest upcoming trigger
|
||||||
|
- **Resilient save flow**: Add/Edit sheets stay open on scheduling failures and preserve current edits
|
||||||
- **Next trigger preview**: Shows when the next alarm will fire
|
- **Next trigger preview**: Shows when the next alarm will fire
|
||||||
- **Responsive time picker**: Font sizes adapt to available space and orientation
|
- **Responsive time picker**: Font sizes adapt to available space and orientation
|
||||||
- **AlarmKitService**: Centralized service for AlarmKit integration
|
- **AlarmKitService**: Centralized service for AlarmKit integration
|
||||||
|
|||||||
@ -141,6 +141,10 @@ Swift access is provided via:
|
|||||||
- Onboarding now updates Keep Awake through `ClockViewModel` (single source of truth) instead of direct `UserDefaults` writes.
|
- Onboarding now updates Keep Awake through `ClockViewModel` (single source of truth) instead of direct `UserDefaults` writes.
|
||||||
- Alarm scheduling failures now surface user-visible errors and rollback failed add/update/toggle operations.
|
- Alarm scheduling failures now surface user-visible errors and rollback failed add/update/toggle operations.
|
||||||
- Added accessibility identifiers for critical controls and updated UI tests to use stable identifiers instead of coordinate heuristics.
|
- Added accessibility identifiers for critical controls and updated UI tests to use stable identifiers instead of coordinate heuristics.
|
||||||
|
- Alarms now support weekday repeat schedules end-to-end, including editor UI, next-trigger calculation, and AlarmKit weekly relative schedules.
|
||||||
|
- Add/Edit alarm sheets now stay open if scheduling fails, preserving user inputs and showing an in-sheet error alert.
|
||||||
|
- Alarm list ordering now prioritizes enabled alarms and sorts by the next trigger time for faster at-a-glance relevance.
|
||||||
|
- Repeat-day controls now follow locale weekday order and include accessibility identifiers for repeat/alert options controls.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -143,7 +143,9 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let symbols = Calendar.current.shortWeekdaySymbols
|
let symbols = Calendar.current.shortWeekdaySymbols
|
||||||
let labels = normalized.compactMap { weekday -> String? in
|
let orderedWeekdays = displayOrderedWeekdays(for: Calendar.current)
|
||||||
|
let labels = orderedWeekdays.compactMap { weekday -> String? in
|
||||||
|
guard weekdaySet.contains(weekday) else { return nil }
|
||||||
guard (1...7).contains(weekday) else { return nil }
|
guard (1...7).contains(weekday) else { return nil }
|
||||||
return symbols[weekday - 1]
|
return symbols[weekday - 1]
|
||||||
}
|
}
|
||||||
@ -153,4 +155,9 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
static func sanitizedWeekdays(_ weekdays: [Int]) -> [Int] {
|
static func sanitizedWeekdays(_ weekdays: [Int]) -> [Int] {
|
||||||
Array(Set(weekdays.filter { (1...7).contains($0) })).sorted()
|
Array(Set(weekdays.filter { (1...7).contains($0) })).sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func displayOrderedWeekdays(for calendar: Calendar) -> [Int] {
|
||||||
|
let first = max(1, min(calendar.firstWeekday, 7))
|
||||||
|
return Array(first...7) + Array(1..<first)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ import Observation
|
|||||||
@Observable
|
@Observable
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AlarmViewModel {
|
final class AlarmViewModel {
|
||||||
|
enum AlarmOperationResult {
|
||||||
|
case success
|
||||||
|
case failure(String)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
private let alarmService: AlarmService
|
private let alarmService: AlarmService
|
||||||
@ -51,7 +55,8 @@ final class AlarmViewModel {
|
|||||||
|
|
||||||
// MARK: - Alarm CRUD Operations
|
// MARK: - Alarm CRUD Operations
|
||||||
|
|
||||||
func addAlarm(_ alarm: Alarm) async {
|
@discardableResult
|
||||||
|
func addAlarm(_ alarm: Alarm, presentErrorAlert: Bool = true) async -> AlarmOperationResult {
|
||||||
alarmService.addAlarm(alarm)
|
alarmService.addAlarm(alarm)
|
||||||
|
|
||||||
// Schedule with AlarmKit if alarm is enabled
|
// Schedule with AlarmKit if alarm is enabled
|
||||||
@ -63,12 +68,19 @@ final class AlarmViewModel {
|
|||||||
Design.debugLog("[alarms] AlarmKit scheduling failed: \(error)")
|
Design.debugLog("[alarms] AlarmKit scheduling failed: \(error)")
|
||||||
// Roll back add so enabled alarms always represent scheduled alarms.
|
// Roll back add so enabled alarms always represent scheduled alarms.
|
||||||
alarmService.deleteAlarm(id: alarm.id)
|
alarmService.deleteAlarm(id: alarm.id)
|
||||||
presentAlarmOperationError(action: "add", error: error)
|
let message = alarmOperationErrorMessage(action: "add", error: error)
|
||||||
|
if presentErrorAlert {
|
||||||
|
presentAlarmOperationError(message: message)
|
||||||
}
|
}
|
||||||
|
return .failure(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAlarm(_ alarm: Alarm) async {
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func updateAlarm(_ alarm: Alarm, presentErrorAlert: Bool = true) async -> AlarmOperationResult {
|
||||||
let previousAlarm = alarmService.getAlarm(id: alarm.id)
|
let previousAlarm = alarmService.getAlarm(id: alarm.id)
|
||||||
alarmService.updateAlarm(alarm)
|
alarmService.updateAlarm(alarm)
|
||||||
|
|
||||||
@ -90,9 +102,15 @@ final class AlarmViewModel {
|
|||||||
alarmKitService.cancelAlarm(id: previousAlarm.id)
|
alarmKitService.cancelAlarm(id: previousAlarm.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
presentAlarmOperationError(action: "update", error: error)
|
let message = alarmOperationErrorMessage(action: "update", error: error)
|
||||||
|
if presentErrorAlert {
|
||||||
|
presentAlarmOperationError(message: message)
|
||||||
|
}
|
||||||
|
return .failure(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAlarm(id: UUID) async {
|
func deleteAlarm(id: UUID) async {
|
||||||
@ -185,13 +203,22 @@ final class AlarmViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func presentAlarmOperationError(action: String, error: Error) {
|
private func presentAlarmOperationError(action: String, error: Error) {
|
||||||
|
let message = alarmOperationErrorMessage(action: action, error: error)
|
||||||
|
presentAlarmOperationError(message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentAlarmOperationError(message: String) {
|
||||||
|
errorAlertMessage = message
|
||||||
|
isShowingErrorAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func alarmOperationErrorMessage(action: String, error: Error) -> String {
|
||||||
let detail = if let localized = error as? LocalizedError,
|
let detail = if let localized = error as? LocalizedError,
|
||||||
let description = localized.errorDescription {
|
let description = localized.errorDescription {
|
||||||
description
|
description
|
||||||
} else {
|
} else {
|
||||||
error.localizedDescription
|
error.localizedDescription
|
||||||
}
|
}
|
||||||
errorAlertMessage = "Unable to \(action) alarm. \(detail)"
|
return "Unable to \(action) alarm. \(detail)"
|
||||||
isShowingErrorAlert = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,9 @@ struct AddAlarmView: View {
|
|||||||
@State private var isVibrationEnabled = true
|
@State private var isVibrationEnabled = true
|
||||||
@State private var isLightFlashEnabled = false
|
@State private var isLightFlashEnabled = false
|
||||||
@State private var volume: Float = 1.0
|
@State private var volume: Float = 1.0
|
||||||
|
@State private var isSaving = false
|
||||||
|
@State private var isShowingSaveErrorAlert = false
|
||||||
|
@State private var saveErrorMessage = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@ -101,6 +104,12 @@ struct AddAlarmView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlertOptionsSection(
|
||||||
|
isVibrationEnabled: $isVibrationEnabled,
|
||||||
|
isLightFlashEnabled: $isLightFlashEnabled,
|
||||||
|
volume: $volume
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
}
|
}
|
||||||
@ -117,8 +126,35 @@ struct AddAlarmView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Save") {
|
Button(isSaving ? "Saving..." : "Save") {
|
||||||
Task {
|
Task {
|
||||||
|
await saveAlarm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(isSaving)
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.accessibilityIdentifier("alarms.add.saveButton")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
|
||||||
|
Button("OK", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
Text(saveErrorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
private func getSoundDisplayName(_ fileName: String) -> String {
|
||||||
|
return AlarmSoundService.shared.getSoundDisplayName(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveAlarm() async {
|
||||||
|
guard !isSaving else { return }
|
||||||
|
isSaving = true
|
||||||
|
defer { isSaving = false }
|
||||||
|
|
||||||
let newAlarm = viewModel.createNewAlarm(
|
let newAlarm = viewModel.createNewAlarm(
|
||||||
time: newAlarmTime,
|
time: newAlarmTime,
|
||||||
repeatWeekdays: repeatWeekdays,
|
repeatWeekdays: repeatWeekdays,
|
||||||
@ -130,20 +166,14 @@ struct AddAlarmView: View {
|
|||||||
isLightFlashEnabled: isLightFlashEnabled,
|
isLightFlashEnabled: isLightFlashEnabled,
|
||||||
volume: volume
|
volume: volume
|
||||||
)
|
)
|
||||||
await viewModel.addAlarm(newAlarm)
|
|
||||||
isPresented = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundStyle(AppAccent.primary)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.accessibilityIdentifier("alarms.add.saveButton")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
let result = await viewModel.addAlarm(newAlarm, presentErrorAlert: false)
|
||||||
private func getSoundDisplayName(_ fileName: String) -> String {
|
switch result {
|
||||||
return AlarmSoundService.shared.getSoundDisplayName(fileName)
|
case .success:
|
||||||
|
isPresented = false
|
||||||
|
case .failure(let message):
|
||||||
|
saveErrorMessage = message
|
||||||
|
isShowingSaveErrorAlert = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ struct AlarmView: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.alarms) { alarm in
|
ForEach(sortedAlarms) { alarm in
|
||||||
AlarmRowView(
|
AlarmRowView(
|
||||||
alarm: alarm,
|
alarm: alarm,
|
||||||
onToggle: {
|
onToggle: {
|
||||||
@ -117,6 +117,26 @@ struct AlarmView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
|
private var sortedAlarms: [Alarm] {
|
||||||
|
viewModel.alarms.sorted { lhs, rhs in
|
||||||
|
if lhs.isEnabled != rhs.isEnabled {
|
||||||
|
return lhs.isEnabled && !rhs.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
let lhsTrigger = lhs.nextTriggerTime()
|
||||||
|
let rhsTrigger = rhs.nextTriggerTime()
|
||||||
|
if lhsTrigger != rhsTrigger {
|
||||||
|
return lhsTrigger < rhsTrigger
|
||||||
|
}
|
||||||
|
|
||||||
|
if lhs.time != rhs.time {
|
||||||
|
return lhs.time < rhs.time
|
||||||
|
}
|
||||||
|
|
||||||
|
return lhs.label.localizedStandardCompare(rhs.label) == .orderedAscending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func deleteAlarm(at offsets: IndexSet) {
|
private func deleteAlarm(at offsets: IndexSet) {
|
||||||
Task {
|
Task {
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// AlertOptionsSection.swift
|
||||||
|
// TheNoiseClock
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 2/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
/// Reusable alarm alert options section for vibration, flash, and volume.
|
||||||
|
struct AlertOptionsSection: View {
|
||||||
|
@Binding var isVibrationEnabled: Bool
|
||||||
|
@Binding var isLightFlashEnabled: Bool
|
||||||
|
@Binding var volume: Float
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section("Alert Options") {
|
||||||
|
Toggle("Vibration", isOn: $isVibrationEnabled)
|
||||||
|
.tint(AppAccent.primary)
|
||||||
|
.accessibilityIdentifier("alarms.alertOptions.vibrationToggle")
|
||||||
|
|
||||||
|
Toggle("Flash Screen", isOn: $isLightFlashEnabled)
|
||||||
|
.tint(AppAccent.primary)
|
||||||
|
.accessibilityIdentifier("alarms.alertOptions.flashToggle")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
|
HStack {
|
||||||
|
Text("Volume")
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int((volume * 100).rounded()))%")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value: Binding(
|
||||||
|
get: { Double(volume) },
|
||||||
|
set: { volume = Float(min(max($0, 0), 1)) }
|
||||||
|
),
|
||||||
|
in: 0...1
|
||||||
|
)
|
||||||
|
.tint(AppAccent.primary)
|
||||||
|
.accessibilityIdentifier("alarms.alertOptions.volumeSlider")
|
||||||
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xxSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,15 +12,15 @@ import Bedrock
|
|||||||
struct RepeatSelectionView: View {
|
struct RepeatSelectionView: View {
|
||||||
@Binding var repeatWeekdays: [Int]
|
@Binding var repeatWeekdays: [Int]
|
||||||
|
|
||||||
private let orderedWeekdays = [1, 2, 3, 4, 5, 6, 7]
|
private let allWeekdays = [1, 2, 3, 4, 5, 6, 7]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section("Quick Picks") {
|
Section("Quick Picks") {
|
||||||
quickPickRow(title: "Once", weekdays: [])
|
quickPickRow(id: "once", title: "Once", weekdays: [])
|
||||||
quickPickRow(title: "Every Day", weekdays: orderedWeekdays)
|
quickPickRow(id: "everyday", title: "Every Day", weekdays: allWeekdays)
|
||||||
quickPickRow(title: "Weekdays", weekdays: [2, 3, 4, 5, 6])
|
quickPickRow(id: "weekdays", title: "Weekdays", weekdays: [2, 3, 4, 5, 6])
|
||||||
quickPickRow(title: "Weekends", weekdays: [1, 7])
|
quickPickRow(id: "weekends", title: "Weekends", weekdays: [1, 7])
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Repeat On") {
|
Section("Repeat On") {
|
||||||
@ -39,6 +39,7 @@ struct RepeatSelectionView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
toggleDay(weekday)
|
toggleDay(weekday)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("alarms.repeat.day.\(weekday)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,8 +54,13 @@ struct RepeatSelectionView: View {
|
|||||||
Alarm.sanitizedWeekdays(repeatWeekdays)
|
Alarm.sanitizedWeekdays(repeatWeekdays)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var orderedWeekdays: [Int] {
|
||||||
|
let first = max(1, min(Calendar.current.firstWeekday, 7))
|
||||||
|
return Array(first...7) + Array(1..<first)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func quickPickRow(title: String, weekdays: [Int]) -> some View {
|
private func quickPickRow(id: String, title: String, weekdays: [Int]) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text(title)
|
Text(title)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
@ -69,6 +75,7 @@ struct RepeatSelectionView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
repeatWeekdays = Alarm.sanitizedWeekdays(weekdays)
|
repeatWeekdays = Alarm.sanitizedWeekdays(weekdays)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("alarms.repeat.quick.\(id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dayName(for weekday: Int) -> String {
|
private func dayName(for weekday: Int) -> String {
|
||||||
@ -87,4 +94,3 @@ struct RepeatSelectionView: View {
|
|||||||
repeatWeekdays = Alarm.sanitizedWeekdays(Array(updated))
|
repeatWeekdays = Alarm.sanitizedWeekdays(Array(updated))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,9 @@ struct EditAlarmView: View {
|
|||||||
@State private var isVibrationEnabled: Bool
|
@State private var isVibrationEnabled: Bool
|
||||||
@State private var isLightFlashEnabled: Bool
|
@State private var isLightFlashEnabled: Bool
|
||||||
@State private var volume: Float
|
@State private var volume: Float
|
||||||
|
@State private var isSaving = false
|
||||||
|
@State private var isShowingSaveErrorAlert = false
|
||||||
|
@State private var saveErrorMessage = ""
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init(viewModel: AlarmViewModel, alarm: Alarm) {
|
init(viewModel: AlarmViewModel, alarm: Alarm) {
|
||||||
@ -131,6 +134,13 @@ struct EditAlarmView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(AppSurface.card)
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
|
AlertOptionsSection(
|
||||||
|
isVibrationEnabled: $isVibrationEnabled,
|
||||||
|
isLightFlashEnabled: $isLightFlashEnabled,
|
||||||
|
volume: $volume
|
||||||
|
)
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
@ -149,12 +159,39 @@ struct EditAlarmView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Save") {
|
Button(isSaving ? "Saving..." : "Save") {
|
||||||
Task {
|
Task {
|
||||||
|
await saveAlarm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(isSaving)
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.accessibilityIdentifier("alarms.edit.saveButton")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
|
||||||
|
Button("OK", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
Text(saveErrorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
private func getSoundDisplayName(_ fileName: String) -> String {
|
||||||
|
return AlarmSoundService.shared.getSoundDisplayName(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveAlarm() async {
|
||||||
|
guard !isSaving else { return }
|
||||||
|
isSaving = true
|
||||||
|
defer { isSaving = false }
|
||||||
|
|
||||||
let updatedAlarm = Alarm(
|
let updatedAlarm = Alarm(
|
||||||
id: alarm.id, // Keep the same ID
|
id: alarm.id,
|
||||||
time: alarmTime,
|
time: alarmTime,
|
||||||
isEnabled: alarm.isEnabled, // Keep the same enabled state
|
isEnabled: alarm.isEnabled,
|
||||||
repeatWeekdays: repeatWeekdays,
|
repeatWeekdays: repeatWeekdays,
|
||||||
soundName: selectedSoundName,
|
soundName: selectedSoundName,
|
||||||
label: alarmLabel,
|
label: alarmLabel,
|
||||||
@ -164,21 +201,15 @@ struct EditAlarmView: View {
|
|||||||
isLightFlashEnabled: isLightFlashEnabled,
|
isLightFlashEnabled: isLightFlashEnabled,
|
||||||
volume: volume
|
volume: volume
|
||||||
)
|
)
|
||||||
await viewModel.updateAlarm(updatedAlarm)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundStyle(AppAccent.primary)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.accessibilityIdentifier("alarms.edit.saveButton")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
let result = await viewModel.updateAlarm(updatedAlarm, presentErrorAlert: false)
|
||||||
private func getSoundDisplayName(_ fileName: String) -> String {
|
switch result {
|
||||||
return AlarmSoundService.shared.getSoundDisplayName(fileName)
|
case .success:
|
||||||
|
dismiss()
|
||||||
|
case .failure(let message):
|
||||||
|
saveErrorMessage = message
|
||||||
|
isShowingSaveErrorAlert = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user