InsulinSensitivityStepView.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. //
  2. // InsulinSensitivityStepView.swift
  3. // Trio
  4. //
  5. // Created by Marvin Polscheit on 19.03.25.
  6. //
  7. import Charts
  8. import SwiftUI
  9. import UIKit
  10. /// Insulin sensitivity step view for setting insulin sensitivity factor.
  11. struct InsulinSensitivityStepView: View {
  12. @Bindable var state: Onboarding.StateModel
  13. @State private var refreshUI = UUID() // to update chart when slider value changes
  14. @State private var therapyItems: [TherapySettingItem] = []
  15. @State private var now = Date()
  16. // For chart scaling
  17. private let chartScale = Calendar.current
  18. .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
  19. private var numberFormatter: NumberFormatter {
  20. let formatter = NumberFormatter()
  21. formatter.numberStyle = .decimal
  22. formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
  23. return formatter
  24. }
  25. private var dateFormatter: DateFormatter {
  26. let formatter = DateFormatter()
  27. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  28. formatter.timeStyle = .short
  29. return formatter
  30. }
  31. var body: some View {
  32. LazyVStack {
  33. VStack(alignment: .leading, spacing: 0) {
  34. // Chart visualization
  35. if !state.isfItems.isEmpty {
  36. VStack(alignment: .leading) {
  37. isfChart
  38. .frame(height: 180)
  39. .padding(.horizontal)
  40. }
  41. .padding(.vertical)
  42. .background(Color.chart.opacity(0.65))
  43. .clipShape(
  44. .rect(
  45. topLeadingRadius: 10,
  46. bottomLeadingRadius: 0,
  47. bottomTrailingRadius: 0,
  48. topTrailingRadius: 10
  49. )
  50. )
  51. }
  52. TherapySettingEditorView(
  53. items: $therapyItems,
  54. unit: state.units == .mgdL ? .mgdLPerUnit : .mmolLPerUnit,
  55. timeOptions: state.isfTimeValues,
  56. valueOptions: state.isfRateValues,
  57. validateOnDelete: state.validateISF
  58. )
  59. // Example calculation based on first ISF
  60. if !state.isfItems.isEmpty {
  61. Spacer(minLength: 20)
  62. VStack(alignment: .leading, spacing: 8) {
  63. Text("Example Calculation")
  64. .font(.headline)
  65. .padding(.horizontal)
  66. VStack(alignment: .leading, spacing: 8) {
  67. // Current glucose is 40 mg/dL or 2.2 mmol/L above target
  68. let aboveTarget = state.units == .mgdL ? Decimal(40) : 40.asMmolL
  69. let firstIsfRate: Decimal = state.isfRateValues[state.isfItems.first?.rateIndex ?? 0]
  70. let isfValue = state.units == .mgdL ? firstIsfRate : firstIsfRate.asMmolL
  71. let insulinNeeded = aboveTarget / isfValue
  72. Text(
  73. "If you are \(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") \(state.units.rawValue) above target:"
  74. )
  75. .font(.subheadline)
  76. .padding(.horizontal)
  77. Text(
  78. "\(aboveTarget.description) \(state.units.rawValue) / \(isfValue.description) \(state.units.rawValue)/\(String(localized: "U", comment: "Insulin unit abbreviation")) = \(String(format: "%.1f", Double(insulinNeeded))) \(String(localized: "U", comment: "Insulin unit abbreviation"))"
  79. )
  80. .font(.system(.body, design: .monospaced))
  81. .foregroundColor(.cyan)
  82. .padding()
  83. .frame(maxWidth: .infinity, alignment: .center)
  84. .background(Color.chart.opacity(0.65))
  85. .cornerRadius(10)
  86. }
  87. }
  88. Spacer(minLength: 20)
  89. // Information about ISF
  90. VStack(alignment: .leading, spacing: 8) {
  91. Text("What This Means")
  92. .font(.headline)
  93. .padding(.horizontal)
  94. VStack(alignment: .leading, spacing: 4) {
  95. let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)" +
  96. "\(state.units.rawValue)"
  97. Text(
  98. "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
  99. )
  100. Text("• A lower number means you're more sensitive to insulin")
  101. Text("• A higher number means you're less sensitive to insulin")
  102. }
  103. .font(.caption)
  104. .foregroundColor(.secondary)
  105. .padding(.horizontal)
  106. }
  107. }
  108. }
  109. }
  110. .onAppear {
  111. if state.isfItems.isEmpty {
  112. state.addInitialISF()
  113. }
  114. state.validateISF()
  115. therapyItems = state.getISFTherapyItems()
  116. }.onChange(of: therapyItems) { _, newItems in
  117. state.updateISF(from: newItems)
  118. refreshUI = UUID()
  119. }
  120. }
  121. // Chart for visualizing ISF profile
  122. private var isfChart: some View {
  123. Chart {
  124. ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
  125. let displayValue = state.isfRateValues[item.rateIndex]
  126. let startDate = Calendar.current
  127. .startOfDay(for: now)
  128. .addingTimeInterval(state.isfTimeValues[item.timeIndex])
  129. var offset: TimeInterval {
  130. if state.isfItems.count > index + 1 {
  131. return state.isfTimeValues[state.isfItems[index + 1].timeIndex]
  132. } else {
  133. return state.isfTimeValues.last! + 30 * 60
  134. }
  135. }
  136. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  137. RectangleMark(
  138. xStart: .value("start", startDate),
  139. xEnd: .value("end", endDate),
  140. yStart: .value("rate-start", displayValue),
  141. yEnd: .value("rate-end", 0)
  142. ).foregroundStyle(
  143. .linearGradient(
  144. colors: [
  145. Color.cyan.opacity(0.6),
  146. Color.cyan.opacity(0.1)
  147. ],
  148. startPoint: .bottom,
  149. endPoint: .top
  150. )
  151. ).alignsMarkStylesWithPlotArea()
  152. LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
  153. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  154. LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
  155. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  156. }
  157. }
  158. .id(refreshUI) // Force chart update
  159. .chartXAxis {
  160. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  161. AxisValueLabel(format: .dateTime.hour())
  162. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  163. }
  164. }
  165. .chartXScale(
  166. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  167. .addingTimeInterval(60 * 60 * 24)
  168. )
  169. .chartYAxis {
  170. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  171. AxisValueLabel()
  172. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  173. }
  174. }
  175. }
  176. }