ISFEditorRootView.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import Charts
  2. import SwiftUI
  3. import Swinject
  4. extension ISFEditor {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @State 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.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. HStack {
  31. if state.shouldDisplaySaving {
  32. ProgressView().padding(.trailing, 10)
  33. }
  34. Button {
  35. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  36. impactHeavy.impactOccurred()
  37. state.save()
  38. // deactivate saving display after 1.25 seconds
  39. DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
  40. state.shouldDisplaySaving = false
  41. }
  42. } label: {
  43. HStack {
  44. if state.shouldDisplaySaving {
  45. ProgressView().padding(.trailing, 10)
  46. }
  47. Text(state.shouldDisplaySaving ? "Saving..." : "Save")
  48. }
  49. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  50. .padding(10)
  51. }
  52. }
  53. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  54. .disabled(shouldDisableButton)
  55. .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
  56. .tint(.white)
  57. .clipShape(RoundedRectangle(cornerRadius: 8))
  58. }
  59. }.padding(5)
  60. }
  61. }
  62. var body: some View {
  63. ScrollViewReader { proxy in
  64. VStack(spacing: 0) {
  65. ScrollView {
  66. LazyVStack {
  67. VStack(alignment: .leading, spacing: 0) {
  68. // Chart visualization
  69. if !state.items.isEmpty {
  70. isfChart
  71. .frame(height: 180)
  72. .padding()
  73. .background(Color.chart.opacity(0.65))
  74. .clipShape(
  75. .rect(
  76. topLeadingRadius: 10,
  77. bottomLeadingRadius: 0,
  78. bottomTrailingRadius: 0,
  79. topTrailingRadius: 10
  80. )
  81. )
  82. .padding(.horizontal)
  83. .padding(.top)
  84. }
  85. // ISF list
  86. TherapySettingEditorView(
  87. items: $state.therapyItems,
  88. unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
  89. timeOptions: state.timeValues,
  90. valueOptions: state.rateValues,
  91. validateOnDelete: state.validate,
  92. onItemAdded: {
  93. withAnimation {
  94. proxy.scrollTo(bottomID, anchor: .bottom)
  95. }
  96. }
  97. )
  98. .padding(.horizontal)
  99. HStack {
  100. Image(systemName: "hand.draw.fill")
  101. .padding(.leading)
  102. Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
  103. .padding(.trailing)
  104. }
  105. .font(.subheadline)
  106. .fontWeight(.light)
  107. .foregroundStyle(.secondary)
  108. .padding()
  109. }
  110. }
  111. }
  112. saveButton
  113. }
  114. .background(appState.trioBackgroundColor(for: colorScheme))
  115. .onAppear(perform: configureView)
  116. .navigationTitle("Insulin Sensitivities")
  117. .navigationBarTitleDisplayMode(.automatic)
  118. .onAppear {
  119. state.validate()
  120. state.therapyItems = state.getTherapyItems()
  121. }
  122. .onChange(of: state.therapyItems) { _, newItems in
  123. state.updateFromTherapyItems(newItems)
  124. refreshUI = UUID()
  125. }
  126. }
  127. }
  128. // Chart for visualizing ISF profile
  129. private var isfChart: some View {
  130. Chart {
  131. ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
  132. let displayValue = state.rateValues[item.rateIndex]
  133. let startDate = Calendar.current
  134. .startOfDay(for: now)
  135. .addingTimeInterval(state.timeValues[item.timeIndex])
  136. var offset: TimeInterval {
  137. if state.items.count > index + 1 {
  138. return state.timeValues[state.items[index + 1].timeIndex]
  139. } else {
  140. return state.timeValues.last! + 30 * 60
  141. }
  142. }
  143. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  144. RectangleMark(
  145. xStart: .value("start", startDate),
  146. xEnd: .value("end", endDate),
  147. yStart: .value("rate-start", displayValue),
  148. yEnd: .value("rate-end", 0)
  149. ).foregroundStyle(
  150. .linearGradient(
  151. colors: [
  152. Color.cyan.opacity(0.6),
  153. Color.cyan.opacity(0.1)
  154. ],
  155. startPoint: .bottom,
  156. endPoint: .top
  157. )
  158. ).alignsMarkStylesWithPlotArea()
  159. LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
  160. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  161. LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
  162. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  163. }
  164. }
  165. .id(refreshUI) // Force chart update
  166. .chartXAxis {
  167. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  168. AxisValueLabel(format: .dateTime.hour())
  169. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  170. }
  171. }
  172. .chartXScale(
  173. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  174. .addingTimeInterval(60 * 60 * 24)
  175. )
  176. .chartYAxis {
  177. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  178. AxisValueLabel()
  179. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  180. }
  181. }
  182. }
  183. }
  184. }