InsulinSensitivityStepView.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. Text(
  97. "• An ISF of \(isfValue) \(state.units.rawValue)/U means 1 U lowers your glucose by \(isfValue) \(state.units.rawValue)"
  98. )
  99. Text("• A lower number means you're less sensitive (more resistant) to insulin")
  100. Text("• A higher number means you're more sensitive (less resistant) to insulin")
  101. }
  102. .font(.caption)
  103. .foregroundColor(.secondary)
  104. .padding(.horizontal)
  105. }
  106. }
  107. }
  108. }
  109. .onAppear {
  110. if state.isfItems.isEmpty {
  111. state.addInitialISF()
  112. }
  113. state.validateISF()
  114. therapyItems = state.getISFTherapyItems()
  115. }.onChange(of: therapyItems) { _, newItems in
  116. state.updateISF(from: newItems)
  117. refreshUI = UUID()
  118. }
  119. }
  120. // Chart for visualizing ISF profile
  121. private var isfChart: some View {
  122. Chart {
  123. ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
  124. let displayValue = state.isfRateValues[item.rateIndex]
  125. let startDate = Calendar.current
  126. .startOfDay(for: now)
  127. .addingTimeInterval(state.isfTimeValues[item.timeIndex])
  128. var offset: TimeInterval {
  129. if state.isfItems.count > index + 1 {
  130. return state.isfTimeValues[state.isfItems[index + 1].timeIndex]
  131. } else {
  132. return state.isfTimeValues.last! + 30 * 60
  133. }
  134. }
  135. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  136. RectangleMark(
  137. xStart: .value("start", startDate),
  138. xEnd: .value("end", endDate),
  139. yStart: .value("rate-start", displayValue),
  140. yEnd: .value("rate-end", 0)
  141. ).foregroundStyle(
  142. .linearGradient(
  143. colors: [
  144. Color.cyan.opacity(0.6),
  145. Color.cyan.opacity(0.1)
  146. ],
  147. startPoint: .bottom,
  148. endPoint: .top
  149. )
  150. ).alignsMarkStylesWithPlotArea()
  151. LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
  152. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  153. LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
  154. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.cyan)
  155. }
  156. }
  157. .id(refreshUI) // Force chart update
  158. .chartXAxis {
  159. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  160. AxisValueLabel(format: .dateTime.hour())
  161. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  162. }
  163. }
  164. .chartXScale(
  165. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  166. .addingTimeInterval(60 * 60 * 24)
  167. )
  168. .chartYAxis {
  169. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  170. AxisValueLabel()
  171. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  172. }
  173. }
  174. }
  175. }