GlucoseTargetStepView.swift 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. //
  2. // GlucoseTargetStepView.swift
  3. // Trio
  4. //
  5. // Created by Marvin Polscheit on 19.03.25.
  6. //
  7. import Charts
  8. import SwiftUI
  9. import UIKit
  10. /// Glucose target step view for setting target glucose range.
  11. struct GlucoseTargetStepView: 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. // Formatter for glucose values
  16. private var numberFormatter: NumberFormatter {
  17. let formatter = NumberFormatter()
  18. formatter.numberStyle = .decimal
  19. formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
  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. // For chart scaling
  29. private let chartScale = Calendar.current
  30. .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
  31. var body: some View {
  32. LazyVStack {
  33. VStack(alignment: .leading, spacing: 0) {
  34. // Chart visualization
  35. if !state.targetItems.isEmpty {
  36. VStack(alignment: .leading) {
  37. glucoseTargetChart
  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. // Glucose target list
  53. TherapySettingEditorView(
  54. items: $therapyItems,
  55. unit: state.units == .mgdL ? .mgdL : .mmolL,
  56. timeOptions: state.targetTimeValues,
  57. valueOptions: state.targetRateValues
  58. )
  59. }
  60. }
  61. .onAppear {
  62. if state.targetItems.isEmpty {
  63. addTarget()
  64. }
  65. state.validateTarget()
  66. therapyItems = state.getTargetTherapyItems(from: state.targetItems)
  67. }.onChange(of: therapyItems) { _, newItems in
  68. state.updateTargets(from: newItems)
  69. refreshUI = UUID()
  70. }
  71. }
  72. // Add initial target
  73. private func addTarget() {
  74. let timeIndex = state.targetTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
  75. let expectedDefault = Decimal(100)
  76. let targetIndex = state.targetRateValues.enumerated()
  77. .min(by: { abs($0.element - expectedDefault) < abs($1.element - expectedDefault) })?
  78. .offset ?? 0
  79. let newItem = TargetsEditor.Item(lowIndex: targetIndex, highIndex: targetIndex, timeIndex: timeIndex)
  80. state.targetItems.append(newItem)
  81. }
  82. // Computed property to check if we can add more targets
  83. private var canAddTarget: Bool {
  84. guard let lastItem = state.targetItems.last else { return true }
  85. return lastItem.timeIndex < state.targetTimeValues.count - 1
  86. }
  87. // Chart for visualizing glucose targets
  88. private var glucoseTargetChart: some View {
  89. Chart {
  90. ForEach(Array(state.targetItems.enumerated()), id: \.element.id) { index, item in
  91. let displayValue = state.targetRateValues[item.lowIndex]
  92. let tzOffset = TimeZone.current.secondsFromGMT() * -1
  93. let startDate = Date(timeIntervalSinceReferenceDate: state.targetTimeValues[item.timeIndex])
  94. .addingTimeInterval(TimeInterval(tzOffset))
  95. let endDate = state.targetItems.count > index + 1 ?
  96. Date(
  97. timeIntervalSinceReferenceDate: state
  98. .targetTimeValues[state.targetItems[index + 1].timeIndex]
  99. )
  100. .addingTimeInterval(TimeInterval(tzOffset)) :
  101. Date(timeIntervalSinceReferenceDate: state.targetTimeValues.last!).addingTimeInterval(30 * 60)
  102. .addingTimeInterval(TimeInterval(tzOffset))
  103. RectangleMark(
  104. xStart: .value("start", startDate),
  105. xEnd: .value("end", endDate),
  106. yStart: .value("rate-start", displayValue),
  107. yEnd: .value("rate-end", 0)
  108. ).foregroundStyle(
  109. .linearGradient(
  110. colors: [
  111. Color.green.opacity(0.6),
  112. Color.green.opacity(0.1)
  113. ],
  114. startPoint: .bottom,
  115. endPoint: .top
  116. )
  117. ).alignsMarkStylesWithPlotArea()
  118. LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
  119. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green)
  120. LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
  121. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green)
  122. }
  123. }
  124. .id(refreshUI) // Force chart update
  125. .chartXAxis {
  126. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  127. AxisValueLabel(format: .dateTime.hour())
  128. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  129. }
  130. }
  131. .chartXScale(
  132. domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
  133. .addingTimeInterval(60 * 60 * 24)
  134. )
  135. .chartYAxis {
  136. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  137. AxisValueLabel()
  138. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  139. }
  140. }
  141. }
  142. }