AlarmAudioSection.swift 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. //
  2. // AlarmAudioSection.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-05-12.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. struct AlarmAudioSection: View {
  10. @Binding var alarm: Alarm
  11. @State private var showingTonePicker = false
  12. var body: some View {
  13. Section(header: Text("Alert Sound")) {
  14. Button {
  15. showingTonePicker = true
  16. } label: {
  17. HStack {
  18. Text("Tone")
  19. Spacer()
  20. Text(alarm.soundFile.displayName)
  21. .foregroundColor(.secondary)
  22. Image(systemName: "chevron.right")
  23. .foregroundColor(.secondary)
  24. }
  25. }
  26. .buttonStyle(.plain)
  27. .sheet(isPresented: $showingTonePicker) {
  28. TonePickerSheet(selected: $alarm.soundFile)
  29. }
  30. AlarmEnumMenuPicker(
  31. title: "Play",
  32. selection: $alarm.playSoundOption,
  33. allowed: PlaySoundOption.allowed(for: alarm.activeOption)
  34. )
  35. AlarmEnumMenuPicker(
  36. title: "Repeat",
  37. selection: $alarm.repeatSoundOption,
  38. allowed: RepeatSoundOption.allowed(for: alarm.activeOption)
  39. )
  40. }.onChange(of: alarm.activeOption) { newActive in
  41. let playAllowed = PlaySoundOption.allowed(for: newActive)
  42. if !playAllowed.contains(alarm.playSoundOption) {
  43. alarm.playSoundOption = playAllowed.last!
  44. }
  45. let repeatAllowed = RepeatSoundOption.allowed(for: newActive)
  46. if !repeatAllowed.contains(alarm.repeatSoundOption) {
  47. alarm.repeatSoundOption = repeatAllowed.last!
  48. }
  49. }
  50. }
  51. }
  52. struct AlarmEnumMenuPicker<E: CaseIterable & Hashable & DayNightDisplayable>: View {
  53. let title: String
  54. @Binding var selection: E
  55. var allowed: [E]
  56. var body: some View {
  57. HStack {
  58. Text(title)
  59. Spacer()
  60. Picker("", selection: $selection) {
  61. ForEach(allowed, id: \.self) { opt in
  62. Text(opt.displayName).tag(opt)
  63. }
  64. }
  65. // if the current selection became invalid, snap to the first allowed
  66. .onAppear { validate() }
  67. .onChange(of: allowed) { _ in validate() }
  68. }
  69. }
  70. private func validate() {
  71. if !allowed.contains(selection), let first = allowed.first {
  72. selection = first
  73. }
  74. }
  75. }
  76. extension AlarmEnumMenuPicker where E: CaseIterable {
  77. init(title: String, selection: Binding<E>) {
  78. self.title = title
  79. _selection = selection
  80. allowed = Array(E.allCases)
  81. }
  82. }
  83. private struct TonePickerSheet: View {
  84. @Binding var selected: SoundFile
  85. @Environment(\.dismiss) private var dismiss
  86. var body: some View {
  87. NavigationView {
  88. List {
  89. ForEach(SoundFile.allCases) { tone in
  90. Button {
  91. selected = tone
  92. AlarmSound.setSoundFile(str: tone.rawValue)
  93. AlarmSound.stop()
  94. AlarmSound.playTest()
  95. } label: {
  96. HStack {
  97. Text(tone.displayName)
  98. if tone == selected {
  99. Spacer()
  100. Image(systemName: "checkmark")
  101. .foregroundColor(.accentColor)
  102. }
  103. }
  104. }
  105. }
  106. }
  107. .navigationTitle("Choose Tone")
  108. .toolbar {
  109. ToolbarItem(placement: .confirmationAction) {
  110. Button("Done") {
  111. AlarmSound.stop()
  112. dismiss()
  113. }
  114. }
  115. }
  116. }
  117. }
  118. }