GlucoseTargetStepView.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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. ScrollView {
  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.45))
  43. .clipShape(
  44. .rect(
  45. topLeadingRadius: 10,
  46. bottomLeadingRadius: 0,
  47. bottomTrailingRadius: 0,
  48. topTrailingRadius: 10
  49. )
  50. )
  51. }
  52. // Glucose target list
  53. TimeValueEditorView(
  54. items: $therapyItems,
  55. unit: state.units.rawValue,
  56. valueOptions: state.targetRateValues
  57. )
  58. }
  59. }
  60. .onAppear {
  61. if state.targetItems.isEmpty {
  62. state.addTarget()
  63. }
  64. therapyItems = state.getTargetTherapyItems(from: state.targetItems)
  65. }.onChange(of: therapyItems) { _, newItems in
  66. state.updateTargets(from: newItems)
  67. refreshUI = UUID()
  68. }
  69. }
  70. // Computed property to check if we can add more targets
  71. private var canAddTarget: Bool {
  72. guard let lastItem = state.targetItems.last else { return true }
  73. return lastItem.timeIndex < state.targetTimeValues.count - 1
  74. }
  75. // Chart for visualizing glucose targets
  76. private var glucoseTargetChart: some View {
  77. Chart {
  78. ForEach(Array(state.targetItems.enumerated()), id: \.element.id) { index, item in
  79. let displayValue = state.targetRateValues[item.lowIndex]
  80. let tzOffset = TimeZone.current.secondsFromGMT() * -1
  81. let startDate = Date(timeIntervalSinceReferenceDate: state.targetTimeValues[item.timeIndex])
  82. .addingTimeInterval(TimeInterval(tzOffset))
  83. let endDate = state.targetItems.count > index + 1 ?
  84. Date(
  85. timeIntervalSinceReferenceDate: state
  86. .targetTimeValues[state.targetItems[index + 1].timeIndex]
  87. )
  88. .addingTimeInterval(TimeInterval(tzOffset)) :
  89. Date(timeIntervalSinceReferenceDate: state.targetTimeValues.last!).addingTimeInterval(30 * 60)
  90. .addingTimeInterval(TimeInterval(tzOffset))
  91. RectangleMark(
  92. xStart: .value("start", startDate),
  93. xEnd: .value("end", endDate),
  94. yStart: .value("rate-start", displayValue),
  95. yEnd: .value("rate-end", 0)
  96. ).foregroundStyle(
  97. .linearGradient(
  98. colors: [
  99. Color.green.opacity(0.6),
  100. Color.green.opacity(0.1)
  101. ],
  102. startPoint: .bottom,
  103. endPoint: .top
  104. )
  105. ).alignsMarkStylesWithPlotArea()
  106. LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
  107. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green)
  108. LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
  109. .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green)
  110. }
  111. }
  112. .id(refreshUI) // Force chart update
  113. .chartXAxis {
  114. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  115. AxisValueLabel(format: .dateTime.hour())
  116. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  117. }
  118. }
  119. .chartXScale(
  120. domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
  121. .addingTimeInterval(60 * 60 * 24)
  122. )
  123. .chartYAxis {
  124. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  125. AxisValueLabel()
  126. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  127. }
  128. }
  129. }
  130. }