CarbRatioStepView.swift 7.7 KB

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