AlarmAudioSection.swift 4.1 KB

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