InsulinSensitivityStepView.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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 isfValue = state.isfRateValues.isEmpty || state.isfItems.isEmpty ?
  70. Double(truncating: 50 as NSNumber) :
  71. Double(
  72. truncating: state
  73. .isfRateValues[state.isfItems.first!.rateIndex] as NSNumber
  74. )
  75. let insulinNeeded = aboveTarget / Decimal(isfValue)
  76. Text(
  77. "If you are \(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") \(state.units.rawValue) above target:"
  78. )
  79. .font(.subheadline)
  80. .padding(.horizontal)
  81. Text(
  82. "\(numberFormatter.string(from: aboveTarget as NSNumber) ?? "--") / \(numberFormatter.string(from: isfValue as NSNumber) ?? "--") = \(String(format: "%.1f", Double(insulinNeeded)))" +
  83. " " + String(localized: "U", comment: "Insulin unit abbreviation")
  84. )
  85. .font(.system(.body, design: .monospaced))
  86. .foregroundColor(.red)
  87. .padding()
  88. .frame(maxWidth: .infinity, alignment: .center)
  89. .background(Color.chart.opacity(0.65))
  90. .cornerRadius(10)
  91. }
  92. }
  93. Spacer(minLength: 20)
  94. // Information about ISF
  95. VStack(alignment: .leading, spacing: 8) {
  96. Text("What This Means")
  97. .font(.headline)
  98. .padding(.horizontal)
  99. VStack(alignment: .leading, spacing: 4) {
  100. let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)" +
  101. "\(state.units.rawValue)"
  102. Text(
  103. "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
  104. )
  105. Text("• A lower number means you're more sensitive to insulin")
  106. Text("• A higher number means you're less sensitive to insulin")
  107. }
  108. .font(.caption)
  109. .foregroundColor(.secondary)
  110. .padding(.horizontal)
  111. }
  112. }
  113. }
  114. }
  115. .onAppear {
  116. if state.isfItems.isEmpty {
  117. state.addInitialISF()
  118. }
  119. state.validateISF()
  120. therapyItems = state.getISFTherapyItems()
  121. }.onChange(of: therapyItems) { _, newItems in
  122. state.updateISF(from: newItems)
  123. refreshUI = UUID()
  124. }
  125. }
  126. // Chart for visualizing ISF profile
  127. private var isfChart: some View {
  128. Chart {
  129. ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
  130. let displayValue = state.isfRateValues[item.rateIndex]
  131. let startDate = Calendar.current
  132. .startOfDay(for: now)
  133. .addingTimeInterval(state.isfTimeValues[item.timeIndex])
  134. var offset: TimeInterval {
  135. if state.isfItems.count > index + 1 {
  136. return state.isfTimeValues[state.isfItems[index + 1].timeIndex]
  137. } else {
  138. return state.isfTimeValues.last! + 30 * 60
  139. }
  140. }
  141. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  142. RectangleMark(
  143. xStart: .value("start", startDate),
  144. xEnd: .value("end", endDate),
  145. yStart: .value("rate-start", displayValue),
  146. yEnd: .value("rate-end", 0)
  147. ).foregroundStyle(
  148. .linearGradient(
  149. colors: [
  150. Color.red.opacity(0.6),
  151. Color.red.opacity(0.1)
  152. ],
  153. startPoint: .bottom,
  154. endPoint: .top
  155. )
  156. ).alignsMarkStylesWithPlotArea()
  157. LineMark(x: .value("End Date", startDate), y: .value("ISF", displayValue))
  158. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.red)
  159. LineMark(x: .value("Start Date", endDate), y: .value("ISF", displayValue))
  160. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.red)
  161. }
  162. }
  163. .id(refreshUI) // Force chart update
  164. .chartXAxis {
  165. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  166. AxisValueLabel(format: .dateTime.hour())
  167. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  168. }
  169. }
  170. .chartXScale(
  171. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  172. .addingTimeInterval(60 * 60 * 24)
  173. )
  174. .chartYAxis {
  175. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  176. AxisValueLabel()
  177. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  178. }
  179. }
  180. }
  181. }