ISFEditorRootView.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. VStack(alignment: .leading) {
  71. isfChart
  72. .frame(height: 180)
  73. .padding(.horizontal)
  74. }
  75. .padding(.vertical)
  76. .background(Color.chart.opacity(0.65))
  77. .clipShape(
  78. .rect(
  79. topLeadingRadius: 10,
  80. bottomLeadingRadius: 0,
  81. bottomTrailingRadius: 0,
  82. topTrailingRadius: 10
  83. )
  84. )
  85. .padding(.horizontal)
  86. .padding(.top)
  87. }
  88. // ISF list
  89. TherapySettingEditorView(
  90. items: $state.therapyItems,
  91. unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
  92. timeOptions: state.timeValues,
  93. valueOptions: state.rateValues,
  94. validateOnDelete: state.validate,
  95. onItemAdded: {
  96. withAnimation {
  97. proxy.scrollTo(bottomID, anchor: .bottom)
  98. }
  99. }
  100. )
  101. .padding(.horizontal)
  102. .id(bottomID)
  103. }
  104. }
  105. }
  106. saveButton
  107. }
  108. .background(appState.trioBackgroundColor(for: colorScheme))
  109. .onAppear(perform: configureView)
  110. .navigationTitle("Insulin Sensitivities")
  111. .navigationBarTitleDisplayMode(.automatic)
  112. .onAppear {
  113. state.validate()
  114. state.therapyItems = state.getTherapyItems()
  115. }
  116. .onChange(of: state.therapyItems) { _, newItems in
  117. state.updateFromTherapyItems(newItems)
  118. refreshUI = UUID()
  119. }
  120. }
  121. }
  122. // Chart for visualizing ISF profile
  123. private var isfChart: some View {
  124. Chart {
  125. ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
  126. let displayValue = state.rateValues[item.rateIndex]
  127. let startDate = Calendar.current
  128. .startOfDay(for: now)
  129. .addingTimeInterval(state.timeValues[item.timeIndex])
  130. var offset: TimeInterval {
  131. if state.items.count > index + 1 {
  132. return state.timeValues[state.items[index + 1].timeIndex]
  133. } else {
  134. return state.timeValues.last! + 30 * 60
  135. }
  136. }
  137. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  138. RectangleMark(
  139. xStart: .value("start", startDate),
  140. xEnd: .value("end", endDate),
  141. yStart: .value("rate-start", displayValue),
  142. yEnd: .value("rate-end", 0)
  143. ).foregroundStyle(
  144. .linearGradient(
  145. colors: [
  146. Color.cyan.opacity(0.6),
  147. Color.cyan.opacity(0.1)
  148. ],
  149. startPoint: .bottom,
  150. endPoint: .top
  151. )
  152. ).alignsMarkStylesWithPlotArea()
  153. LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
  154. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  155. LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
  156. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  157. }
  158. }
  159. .id(refreshUI) // Force chart update
  160. .chartXAxis {
  161. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  162. AxisValueLabel(format: .dateTime.hour())
  163. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  164. }
  165. }
  166. .chartXScale(
  167. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  168. .addingTimeInterval(60 * 60 * 24)
  169. )
  170. .chartYAxis {
  171. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  172. AxisValueLabel()
  173. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  174. }
  175. }
  176. }
  177. }
  178. }