AlarmListView.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. //
  2. // AlarmListView.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-04-21.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. struct AddAlarmSheet: View {
  10. let onSelect: (AlarmType) -> Void
  11. @Environment(\.dismiss) private var dismiss
  12. private let columns = [
  13. GridItem(.adaptive(minimum: 110), spacing: 16)
  14. ]
  15. var body: some View {
  16. NavigationStack {
  17. ScrollView {
  18. LazyVGrid(columns: columns, spacing: 16) {
  19. ForEach(AlarmType.Group.allCases, id: \.self) { group in
  20. if AlarmType.allCases.contains(where: { $0.group == group }) {
  21. Section(header:
  22. Text(group.rawValue)
  23. .font(.headline)
  24. .frame(maxWidth: .infinity, alignment: .leading)
  25. .padding(.horizontal, 4)
  26. ) {
  27. ForEach(AlarmType.allCases.filter { $0.group == group },
  28. id: \.self) { type in
  29. AlarmTile(type: type) {
  30. onSelect(type)
  31. // dismiss()
  32. }
  33. }
  34. }
  35. }
  36. }
  37. }
  38. .padding()
  39. }
  40. .navigationTitle("Add Alarm")
  41. .toolbar {
  42. ToolbarItem(placement: .cancellationAction) {
  43. Button("Cancel") { dismiss() }
  44. }
  45. }
  46. }
  47. }
  48. }
  49. private struct AlarmTile: View {
  50. let type: AlarmType
  51. let action: () -> Void
  52. var body: some View {
  53. Button(action: action) {
  54. VStack(spacing: 8) {
  55. Image(systemName: type.icon)
  56. .font(.title2)
  57. .foregroundColor(.accentColor)
  58. Text(type.rawValue)
  59. .font(.subheadline)
  60. .multilineTextAlignment(.center)
  61. .lineLimit(2)
  62. Text(type.blurb)
  63. .font(.caption2)
  64. .foregroundColor(.secondary)
  65. .multilineTextAlignment(.center)
  66. .lineLimit(2)
  67. }
  68. .padding()
  69. .frame(maxWidth: .infinity, minHeight: 110)
  70. .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
  71. }
  72. .buttonStyle(.plain)
  73. }
  74. }
  75. private enum SheetInfo: Identifiable {
  76. case picker
  77. case editor(id: UUID, isNew: Bool)
  78. var id: UUID {
  79. switch self {
  80. case .picker:
  81. return UUID(uuidString:"00000000-0000-0000-0000-000000000000")!
  82. case .editor(let id, _):
  83. return id
  84. }
  85. }
  86. }
  87. struct AlarmListView: View {
  88. @ObservedObject private var store = Storage.shared.alarms
  89. @Environment(\.dismiss) private var dismiss
  90. @State private var sheetInfo: SheetInfo?
  91. @State private var deleteAfterDismiss: UUID?
  92. var body: some View {
  93. NavigationStack {
  94. List {
  95. ForEach(store.value) { alarm in
  96. NavigationLink(alarm.name) {
  97. AlarmEditor(alarm: binding(for: alarm))
  98. }
  99. }
  100. .onDelete { store.value.remove(atOffsets: $0) }
  101. }
  102. .navigationTitle("Alarms")
  103. .toolbar {
  104. ToolbarItem(placement: .navigationBarLeading) {
  105. Button("Done") { dismiss() }
  106. }
  107. ToolbarItem(placement: .navigationBarTrailing) {
  108. Button { sheetInfo = .picker } label: { Image(systemName:"plus") }
  109. }
  110. }
  111. .sheet(item: $sheetInfo,
  112. onDismiss: handleSheetDismiss) { info in
  113. sheetContent(for: info)
  114. }
  115. }
  116. }
  117. private func handleSheetDismiss() {
  118. if let id = deleteAfterDismiss,
  119. let idx = store.value.firstIndex(where: { $0.id == id }) {
  120. store.value.remove(at: idx)
  121. }
  122. deleteAfterDismiss = nil
  123. }
  124. @ViewBuilder
  125. private func sheetContent(for info: SheetInfo) -> some View {
  126. switch info {
  127. case .picker:
  128. AddAlarmSheet { type in
  129. let new = Alarm(type: type)
  130. store.value.append(new)
  131. sheetInfo = .editor(id: new.id, isNew: true)
  132. }
  133. case .editor(let id, let isNew):
  134. if let idx = store.value.firstIndex(where: { $0.id == id }) {
  135. AlarmEditor(
  136. alarm: $store.value[idx],
  137. isNew: isNew,
  138. onDone: { sheetInfo = nil },
  139. onCancel: {
  140. deleteAfterDismiss = id
  141. sheetInfo = nil
  142. }
  143. )
  144. } else {
  145. Text("Alarm not found").padding()
  146. }
  147. }
  148. }
  149. private func binding(for alarm: Alarm) -> Binding<Alarm> {
  150. guard let idx = store.value.firstIndex(where: { $0.id == alarm.id }) else {
  151. fatalError("Alarm not found")
  152. }
  153. return $store.value[idx]
  154. }
  155. }