GlucoseTargetStepView.swift 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. //
  2. // GlucoseTargetStepView.swift
  3. // Trio
  4. //
  5. // Created by Marvin Polscheit on 19.03.25.
  6. //
  7. import Charts
  8. import Foundation
  9. import SwiftUI
  10. import UIKit
  11. /// Glucose target step view for setting target glucose range.
  12. struct GlucoseTargetStepView: View {
  13. @Bindable var state: Onboarding.StateModel
  14. @State private var refreshUI = UUID() // to update chart when slider value changes
  15. @State private var therapyItems: [TherapySettingItem] = []
  16. @State private var now = Date()
  17. // Formatter for glucose values
  18. private var numberFormatter: NumberFormatter {
  19. let formatter = NumberFormatter()
  20. formatter.numberStyle = .decimal
  21. formatter.maximumFractionDigits = state.units == .mmolL ? 1 : 0
  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. LazyVStack {
  32. VStack(alignment: .leading, spacing: 0) {
  33. // Chart visualization
  34. if !state.targetItems.isEmpty {
  35. VStack(alignment: .leading) {
  36. glucoseTargetChart
  37. .frame(height: 180)
  38. .padding(.horizontal)
  39. }
  40. .padding(.vertical)
  41. .background(Color.chart.opacity(0.65))
  42. .clipShape(
  43. .rect(
  44. topLeadingRadius: 10,
  45. bottomLeadingRadius: 0,
  46. bottomTrailingRadius: 0,
  47. topTrailingRadius: 10
  48. )
  49. )
  50. }
  51. // Glucose target list
  52. TherapySettingEditorView(
  53. items: $therapyItems,
  54. unit: state.units == .mgdL ? .mgdL : .mmolL,
  55. timeOptions: state.targetTimeValues,
  56. valueOptions: state.targetRateValues,
  57. validateOnDelete: state.validateTarget
  58. )
  59. }
  60. }
  61. .onAppear {
  62. if state.targetItems.isEmpty {
  63. state.addInitialTarget()
  64. }
  65. state.validateTarget()
  66. therapyItems = state.getTargetTherapyItems()
  67. }.onChange(of: therapyItems) { _, newItems in
  68. state.updateTargets(from: newItems)
  69. refreshUI = UUID()
  70. }
  71. }
  72. // Chart for visualizing glucose targets
  73. private var glucoseTargetChart: some View {
  74. Chart {
  75. ForEach(Array(state.targetItems.enumerated()), id: \.element.id) { index, item in
  76. let rawValue = state.targetRateValues[item.lowIndex]
  77. let displayValue = state.units == .mgdL ? rawValue : rawValue.asMmolL
  78. let startDate = Calendar.current
  79. .startOfDay(for: now)
  80. .addingTimeInterval(state.targetTimeValues[item.timeIndex])
  81. var offset: TimeInterval {
  82. if state.targetItems.count > index + 1 {
  83. return state.targetTimeValues[state.targetItems[index + 1].timeIndex]
  84. } else {
  85. return state.targetTimeValues.last! + 30 * 60
  86. }
  87. }
  88. let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
  89. LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
  90. .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green)
  91. LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
  92. .lineStyle(.init(lineWidth: 2.5)).foregroundStyle(Color.green)
  93. }
  94. }
  95. .id(refreshUI) // Force chart update
  96. .chartXAxis {
  97. AxisMarks(values: .automatic(desiredCount: 6)) { _ in
  98. AxisValueLabel(format: .dateTime.hour())
  99. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  100. }
  101. }
  102. .chartXScale(
  103. domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
  104. .addingTimeInterval(60 * 60 * 24)
  105. )
  106. .chartYAxis {
  107. AxisMarks(values: .automatic(desiredCount: 4)) { _ in
  108. AxisValueLabel()
  109. AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
  110. }
  111. }
  112. .chartYScale(
  113. domain: (state.units == .mgdL ? Decimal(72) : Decimal(72).asMmolL) ...
  114. (state.units == .mgdL ? Decimal(180) : Decimal(180).asMmolL)
  115. )
  116. }
  117. }