TargetsEditorRootView.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import Charts
  2. import SwiftUI
  3. import Swinject
  4. extension TargetsEditor {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @StateObject var state = StateModel()
  8. @State private var refreshUI = UUID()
  9. @State private var now = Date()
  10. @Namespace private var bottomID
  11. @Environment(\.colorScheme) var colorScheme
  12. @Environment(AppState.self) var appState
  13. private var numberFormatter: NumberFormatter {
  14. let formatter = NumberFormatter()
  15. formatter.numberStyle = .decimal
  16. formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
  17. return formatter
  18. }
  19. var saveButton: some View {
  20. ZStack {
  21. let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
  22. Rectangle()
  23. .frame(width: UIScreen.main.bounds.width, height: 65)
  24. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  25. .background(.thinMaterial)
  26. .opacity(0.8)
  27. .clipShape(Rectangle())
  28. Group {
  29. HStack {
  30. Button(action: {
  31. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  32. impactHeavy.impactOccurred()
  33. state.save()
  34. // deactivate saving display after 1.25 seconds
  35. DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
  36. state.shouldDisplaySaving = false
  37. }
  38. }, label: {
  39. HStack {
  40. if state.shouldDisplaySaving {
  41. ProgressView().padding(.trailing, 10)
  42. }
  43. Text(state.shouldDisplaySaving ? "Saving..." : "Save")
  44. }
  45. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  46. .padding(10)
  47. })
  48. .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
  49. .disabled(shouldDisableButton)
  50. .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
  51. .tint(.white)
  52. .clipShape(RoundedRectangle(cornerRadius: 8))
  53. }
  54. }.padding(5)
  55. }
  56. }
  57. var body: some View {
  58. ScrollViewReader { proxy in
  59. VStack(spacing: 0) {
  60. ScrollView {
  61. LazyVStack {
  62. VStack(alignment: .leading, spacing: 0) {
  63. // Chart visualization
  64. if !state.items.isEmpty {
  65. glucoseTargetChart
  66. .frame(height: 180)
  67. .padding()
  68. .background(Color.chart.opacity(0.65))
  69. .clipShape(
  70. .rect(
  71. topLeadingRadius: 10,
  72. bottomLeadingRadius: 0,
  73. bottomTrailingRadius: 0,
  74. topTrailingRadius: 10
  75. )
  76. )
  77. .padding(.horizontal)
  78. .padding(.top)
  79. }
  80. // Glucose target list
  81. TherapySettingEditorView(
  82. items: $state.therapyItems,
  83. unit: state.units == .mgdL ? .mgdL : .mmolL,
  84. timeOptions: state.timeValues,
  85. valueOptions: state.rateValues,
  86. validateOnDelete: state.validate,
  87. onItemAdded: {
  88. withAnimation {
  89. proxy.scrollTo(bottomID, anchor: .bottom)
  90. }
  91. }
  92. )
  93. .padding(.horizontal)
  94. .id(bottomID)
  95. HStack {
  96. Image(systemName: "hand.draw.fill")
  97. .padding(.leading)
  98. Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
  99. .padding(.trailing)
  100. }
  101. .font(.subheadline)
  102. .fontWeight(.light)
  103. .foregroundStyle(.secondary)
  104. .padding()
  105. }
  106. }
  107. }
  108. saveButton
  109. }
  110. .background(appState.trioBackgroundColor(for: colorScheme))
  111. .onAppear(perform: configureView)
  112. .navigationTitle("Glucose Targets")
  113. .navigationBarTitleDisplayMode(.automatic)
  114. .onAppear {
  115. state.validate()
  116. state.therapyItems = state.getTherapyItems()
  117. }
  118. .onChange(of: state.therapyItems) { _, newItems in
  119. state.updateFromTherapyItems(newItems)
  120. refreshUI = UUID()
  121. }
  122. }
  123. }
  124. // Chart for visualizing glucose targets
  125. private var glucoseTargetChart: some View {
  126. Chart {
  127. ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
  128. let rawValue = state.rateValues[item.lowIndex]
  129. let displayValue = state.units == .mgdL ? rawValue : rawValue.asMmolL
  130. let startDate = Calendar.current
  131. .startOfDay(for: now)
  132. .addingTimeInterval(state.timeValues[item.timeIndex])
  133. var offset: TimeInterval {
  134. if state.items.count > index + 1 {
  135. return state.timeValues[state.items[index + 1].timeIndex]
  136. } else {
  137. return state.timeValues.last! + 30 * 60
  138. }
  139. }
  140. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  141. LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
  142. .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green)
  143. LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
  144. .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green)
  145. }
  146. }
  147. .id(refreshUI) // Force chart update
  148. .chartXAxis {
  149. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  150. AxisValueLabel(format: .dateTime.hour())
  151. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  152. }
  153. }
  154. .chartXScale(
  155. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  156. .addingTimeInterval(60 * 60 * 24)
  157. )
  158. .chartYAxis {
  159. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  160. AxisValueLabel()
  161. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  162. }
  163. }
  164. .chartYScale(
  165. domain: (state.units == .mgdL ? Decimal(72) : Decimal(72).asMmolL) ...
  166. (state.units == .mgdL ? Decimal(180) : Decimal(180).asMmolL)
  167. )
  168. }
  169. }
  170. }