TimeValueEditorView.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import SwiftUI
  2. struct TimeValueEditorView: View {
  3. @Binding var items: [TherapySettingItem]
  4. var unit: String
  5. var valueOptions: [Decimal]
  6. @State private var selectedItemID: UUID?
  7. var body: some View {
  8. List {
  9. HStack {
  10. Text("Entries").bold()
  11. Spacer()
  12. Button {
  13. let lastTime = items.last?.time ?? 0
  14. let newTime = min(lastTime + 1800, 23 * 3600 + 1800)
  15. let newValue = items.last?.value ?? 1.0
  16. items.append(TherapySettingItem(time: newTime, value: newValue))
  17. } label: {
  18. HStack {
  19. Image(systemName: "plus.circle.fill")
  20. Text("Add")
  21. }.foregroundColor(.accentColor)
  22. }
  23. .disabled(items.count >= 48)
  24. }
  25. .listRowBackground(Color.chart.opacity(0.45))
  26. .padding(.vertical, 5)
  27. ForEach($items) { $item in
  28. VStack(spacing: 0) {
  29. Button {
  30. selectedItemID = selectedItemID == item.id ? nil : item.id
  31. } label: {
  32. HStack {
  33. HStack {
  34. Text("\(item.value, specifier: "%.1f")")
  35. .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
  36. Text(unit.description)
  37. .foregroundStyle(Color.secondary)
  38. }
  39. Spacer()
  40. HStack {
  41. Text("starts at").foregroundStyle(Color.secondary)
  42. let startDate = Date(timeIntervalSinceReferenceDate: item.time)
  43. Text(timeFormatter.string(from: startDate))
  44. .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
  45. }
  46. }.contentShape(Rectangle())
  47. }
  48. .buttonStyle(.plain)
  49. if selectedItemID == item.id {
  50. TimeValuePickerRow(
  51. item: $item,
  52. valueOptions: valueOptions,
  53. unit: unit
  54. )
  55. .transition(.slide)
  56. }
  57. }
  58. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  59. if let index = items.firstIndex(where: { $0.id == item.id }), items.count > 1 {
  60. Button(role: .destructive) {
  61. items.remove(at: index)
  62. selectedItemID = nil
  63. } label: {
  64. Label("Delete", systemImage: "trash")
  65. }
  66. }
  67. }
  68. }
  69. .listRowBackground(Color.chart.opacity(0.45))
  70. Rectangle().fill(Color.chart.opacity(0.45)).frame(height: 10)
  71. .clipShape(
  72. .rect(
  73. topLeadingRadius: 0,
  74. bottomLeadingRadius: 10,
  75. bottomTrailingRadius: 10,
  76. topTrailingRadius: 0
  77. )
  78. )
  79. .listRowBackground(Color.clear)
  80. .listRowInsets(EdgeInsets(top: -22, leading: 0, bottom: 0, trailing: 0))
  81. .listRowSeparator(.hidden)
  82. }
  83. .listStyle(.plain)
  84. .scrollDisabled(true)
  85. .scrollContentBackground(.hidden)
  86. .frame(height: 55 + CGFloat(items.count) * 45 + (items.contains(where: { $0.id == selectedItemID }) ? 230 : 0))
  87. // 55 for header row, item counts x 45 for every entry row + 230 for a visible picker row
  88. }
  89. private var timeFormatter: DateFormatter {
  90. let formatter = DateFormatter()
  91. formatter.dateFormat = "HH:mm"
  92. return formatter
  93. }
  94. }
  95. struct TherapySettingItem: Identifiable, Equatable {
  96. var id = UUID()
  97. var time: TimeInterval // seconds since start of day
  98. var value: Double
  99. }
  100. struct TimeValuePickerRow: View {
  101. @Binding var item: TherapySettingItem
  102. var valueOptions: [Decimal]
  103. var unit: String
  104. var body: some View {
  105. VStack(spacing: 8) {
  106. HStack {
  107. Picker("Time", selection: Binding(
  108. get: { item.time },
  109. set: { item.time = $0 }
  110. )) {
  111. ForEach(0 ..< 48) { i in
  112. let seconds = Double(i * 30 * 60)
  113. Text(timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: seconds)))
  114. .tag(seconds)
  115. }
  116. }
  117. .frame(maxWidth: .infinity)
  118. .clipped()
  119. Picker("Value", selection: Binding(
  120. get: { item.value },
  121. set: { item.value = $0 }
  122. )) {
  123. ForEach(valueOptions, id: \.self) { value in
  124. Text("\(Double(value), specifier: "%.1f") \(unit)").tag(Double(value))
  125. }
  126. }
  127. .frame(maxWidth: .infinity)
  128. .clipped()
  129. }
  130. .pickerStyle(.wheel)
  131. }
  132. .padding(.vertical, 8)
  133. }
  134. private var timeFormatter: DateFormatter {
  135. let formatter = DateFormatter()
  136. formatter.dateFormat = "HH:mm"
  137. formatter.timeZone = TimeZone.current
  138. return formatter
  139. }
  140. }
  141. #Preview {
  142. @Previewable @State var previewItems = [
  143. TherapySettingItem(time: 0, value: 1.0),
  144. TherapySettingItem(time: 1800, value: 1.2)
  145. ]
  146. TimeValueEditorView(
  147. items: $previewItems,
  148. unit: "U/h",
  149. valueOptions: stride(from: 0.0, through: 10.0, by: 0.05).map { Decimal(round(100 * $0) / 100) }
  150. )
  151. }