CarbRatioStepView.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. //
  2. // CarbRatioStepView.swift
  3. // Trio
  4. //
  5. // Created by Marvin Polscheit on 19.03.25.
  6. //
  7. import Charts
  8. import SwiftUI
  9. import UIKit
  10. /// Carb ratio step view for setting insulin-to-carb ratio.
  11. struct CarbRatioStepView: 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. private var formatter: NumberFormatter {
  17. let formatter = NumberFormatter()
  18. formatter.numberStyle = .decimal
  19. formatter.maximumFractionDigits = 1
  20. return formatter
  21. }
  22. private var dateFormatter: DateFormatter {
  23. let formatter = DateFormatter()
  24. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  25. formatter.timeStyle = .short
  26. return formatter
  27. }
  28. var body: some View {
  29. LazyVStack {
  30. VStack(alignment: .leading, spacing: 0) {
  31. // Chart visualization
  32. if !state.carbRatioItems.isEmpty {
  33. VStack(alignment: .leading) {
  34. carbRatioChart
  35. .frame(height: 180)
  36. .padding(.horizontal)
  37. }
  38. .padding(.vertical)
  39. .background(Color.chart.opacity(0.65))
  40. .clipShape(
  41. .rect(
  42. topLeadingRadius: 10,
  43. bottomLeadingRadius: 0,
  44. bottomTrailingRadius: 0,
  45. topTrailingRadius: 10
  46. )
  47. )
  48. }
  49. TherapySettingEditorView(
  50. items: $therapyItems,
  51. unit: .gramPerUnit,
  52. timeOptions: state.carbRatioTimeValues,
  53. valueOptions: state.carbRatioRateValues,
  54. validateOnDelete: state.validateCarbRatios
  55. )
  56. // Example calculation based on first carb ratio
  57. if !state.carbRatioItems.isEmpty {
  58. Spacer(minLength: 20)
  59. VStack(alignment: .leading, spacing: 8) {
  60. Text("Example Calculation")
  61. .font(.headline)
  62. .padding(.horizontal)
  63. VStack(alignment: .leading, spacing: 8) {
  64. Text("For 45g of carbs, you would need:")
  65. .font(.subheadline)
  66. .padding(.horizontal)
  67. let insulinNeeded = 45 /
  68. Double(
  69. truncating: state
  70. .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
  71. )
  72. Text(
  73. "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") = \(String(format: "%.1f", insulinNeeded))" +
  74. " " + String(localized: "U", comment: "Insulin unit abbreviation")
  75. )
  76. .font(.system(.body, design: .monospaced))
  77. .foregroundColor(.orange)
  78. .padding()
  79. .frame(maxWidth: .infinity, alignment: .center)
  80. .background(Color.chart.opacity(0.65))
  81. .cornerRadius(10)
  82. }
  83. }
  84. Spacer(minLength: 20)
  85. // Information about the carb ratio
  86. VStack(alignment: .leading, spacing: 8) {
  87. Text("What This Means")
  88. .font(.headline)
  89. .padding(.horizontal)
  90. VStack(alignment: .leading, spacing: 4) {
  91. Text("• A ratio of 10 g/U means 1 unit of insulin covers 10g of carbs")
  92. Text("• A lower number means you need more insulin for the same amount of carbs")
  93. Text("• A higher number means you need less insulin for the same amount of carbs")
  94. Text("• Different times of day may require different ratios")
  95. }
  96. .font(.caption)
  97. .foregroundColor(.secondary)
  98. .padding(.horizontal)
  99. }
  100. }
  101. }
  102. }
  103. .onAppear {
  104. if state.carbRatioItems.isEmpty {
  105. state.addInitialCarbRatio()
  106. }
  107. state.validateCarbRatios()
  108. therapyItems = state.getCarbRatioTherapyItems()
  109. }.onChange(of: therapyItems) { _, newItems in
  110. state.updateCarbRatio(from: newItems)
  111. refreshUI = UUID()
  112. }
  113. }
  114. // Chart for visualizing carb ratios
  115. private var carbRatioChart: some View {
  116. Chart {
  117. ForEach(Array(state.carbRatioItems.enumerated()), id: \.element.id) { index, item in
  118. let displayValue = state.carbRatioRateValues[item.rateIndex]
  119. let startDate = Calendar.current
  120. .startOfDay(for: now)
  121. .addingTimeInterval(state.carbRatioTimeValues[item.timeIndex])
  122. var offset: TimeInterval {
  123. if state.carbRatioItems.count > index + 1 {
  124. return state.carbRatioTimeValues[state.carbRatioItems[index + 1].timeIndex]
  125. } else {
  126. return state.carbRatioTimeValues.last! + 30 * 60
  127. }
  128. }
  129. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  130. RectangleMark(
  131. xStart: .value("start", startDate),
  132. xEnd: .value("end", endDate),
  133. yStart: .value("rate-start", displayValue),
  134. yEnd: .value("rate-end", 0)
  135. ).foregroundStyle(
  136. .linearGradient(
  137. colors: [
  138. Color.orange.opacity(0.6),
  139. Color.orange.opacity(0.1)
  140. ],
  141. startPoint: .bottom,
  142. endPoint: .top
  143. )
  144. ).alignsMarkStylesWithPlotArea()
  145. LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
  146. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
  147. LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
  148. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
  149. }
  150. }
  151. .id(refreshUI) // Force chart update
  152. .chartXAxis {
  153. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  154. AxisValueLabel(format: .dateTime.hour())
  155. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  156. }
  157. }
  158. .chartXScale(
  159. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  160. .addingTimeInterval(60 * 60 * 24)
  161. )
  162. .chartYAxis {
  163. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  164. AxisValueLabel()
  165. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  166. }
  167. }
  168. }
  169. }