UnitsConfigurationView.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. // LoopFollow
  2. // UnitsConfigurationView.swift
  3. import SwiftUI
  4. /// Reusable view for configuring units and metrics.
  5. /// Can be embedded in Forms or used standalone during onboarding.
  6. ///
  7. /// Onboarding splits this into two lighter pages — the glucose unit on its own,
  8. /// and the LoopFollow statistics (range, glycemic, variability) on a second page
  9. /// — by rendering a subset of the sections via `sections`. Settings shows them
  10. /// all together (the default).
  11. struct UnitsConfigurationView: View {
  12. /// Which groups of sections to render. Onboarding uses `.units` and
  13. /// `.statistics` on separate pages; everywhere else uses `.all`.
  14. struct Sections: OptionSet {
  15. let rawValue: Int
  16. /// Glucose display unit (mg/dL vs mmol/L).
  17. static let units = Sections(rawValue: 1 << 0)
  18. /// Range mode plus the glycemic and variability metrics.
  19. static let statistics = Sections(rawValue: 1 << 1)
  20. static let all: Sections = [.units, .statistics]
  21. }
  22. var sections: Sections = .all
  23. @State private var rangeMode = UnitSettingsStore.shared.timeInRangeMode
  24. @State private var glucoseUnit = UnitSettingsStore.shared.glucoseUnit
  25. @State private var lowValue = Storage.shared.lowLine.value
  26. @State private var highValue = Storage.shared.highLine.value
  27. /// Formats a mg/dL threshold pair in the currently selected glucose unit,
  28. /// e.g. "70–180 mg/dL" or "3.9–10.0 mmol/L".
  29. private func rangeBounds(_ lowMgdl: Double, _ highMgdl: Double) -> String {
  30. let factor = glucoseUnit == .mmolL ? GlucoseConversion.mgDlToMmolL : 1.0
  31. let digits = glucoseUnit.fractionDigits
  32. let low = Localizer.formatToLocalizedString(lowMgdl * factor, maxFractionDigits: digits, minFractionDigits: digits)
  33. let high = Localizer.formatToLocalizedString(highMgdl * factor, maxFractionDigits: digits, minFractionDigits: digits)
  34. return "\(low)–\(high) \(glucoseUnit.rawValue)"
  35. }
  36. var body: some View {
  37. Group {
  38. if sections.contains(.units) {
  39. unitsSections
  40. }
  41. if sections.contains(.statistics) {
  42. statisticsSections
  43. }
  44. }
  45. }
  46. @ViewBuilder
  47. private var unitsSections: some View {
  48. Section("Glucose") {
  49. Picker("Glucose Unit", selection: $glucoseUnit) {
  50. Text("mg/dL").tag(GlucoseDisplayUnit.mgdL)
  51. Text("mmol/L").tag(GlucoseDisplayUnit.mmolL)
  52. }
  53. .pickerStyle(.segmented)
  54. .onChange(of: glucoseUnit) { newValue in
  55. UnitSettingsStore.shared.glucoseUnit = newValue
  56. }
  57. }
  58. }
  59. @ViewBuilder
  60. private var statisticsSections: some View {
  61. Group {
  62. Section {
  63. Picker("Range Mode", selection: $rangeMode) {
  64. Text("TIR").tag(TimeInRangeDisplayMode.tir)
  65. Text("TITR").tag(TimeInRangeDisplayMode.titr)
  66. Text("Custom").tag(TimeInRangeDisplayMode.custom)
  67. }
  68. .pickerStyle(.segmented)
  69. .onChange(of: rangeMode) { newValue in
  70. UnitSettingsStore.shared.timeInRangeMode = newValue
  71. Observable.shared.chartSettingsChanged.value = true
  72. }
  73. if rangeMode == .custom {
  74. BGPicker(
  75. title: String(localized: "Low"),
  76. range: 40 ... 120,
  77. value: $lowValue
  78. )
  79. .id(glucoseUnit)
  80. .onChange(of: lowValue) { newValue in
  81. Storage.shared.lowLine.value = newValue
  82. Observable.shared.chartSettingsChanged.value = true
  83. }
  84. BGPicker(
  85. title: String(localized: "High"),
  86. range: 120 ... 400,
  87. value: $highValue
  88. )
  89. .id(glucoseUnit)
  90. .onChange(of: highValue) { newValue in
  91. Storage.shared.highLine.value = newValue
  92. Observable.shared.chartSettingsChanged.value = true
  93. }
  94. }
  95. } header: {
  96. Text("Range")
  97. } footer: {
  98. Text("TIR — Time in Range, the share of readings within \(rangeBounds(70, 180)).\nTITR — Time in Tight Range, within \(rangeBounds(70, 140)).\nCustom — set your own low and high.")
  99. }
  100. Section {
  101. Picker("Metric", selection: Binding(
  102. get: { UnitSettingsStore.shared.glycemicMetricMode },
  103. set: { UnitSettingsStore.shared.glycemicMetricMode = $0 }
  104. )) {
  105. Text("eHbA1c").tag(GlycemicMetricMode.ehba1c)
  106. Text("GMI").tag(GlycemicMetricMode.gmi)
  107. }
  108. .pickerStyle(.segmented)
  109. Picker("Output Unit", selection: Binding(
  110. get: { UnitSettingsStore.shared.glycemicOutputUnit },
  111. set: { UnitSettingsStore.shared.glycemicOutputUnit = $0 }
  112. )) {
  113. Text("%").tag(GlycemicOutputUnit.percent)
  114. Text("mmol/mol").tag(GlycemicOutputUnit.mmolMol)
  115. }
  116. .pickerStyle(.segmented)
  117. } header: {
  118. Text("Glycemic Metrics")
  119. } footer: {
  120. Text("eHbA1c — an A1c estimate from your average glucose. GMI — Glucose Management Indicator, another A1c estimate from average glucose. % and mmol/mol (IFCC) are two scales for the result.")
  121. }
  122. Section {
  123. Picker("Metric", selection: Binding(
  124. get: { UnitSettingsStore.shared.variabilityMetricMode },
  125. set: { UnitSettingsStore.shared.variabilityMetricMode = $0 }
  126. )) {
  127. Text("Std Dev").tag(VariabilityMetricMode.stdDeviation)
  128. Text("CV").tag(VariabilityMetricMode.cv)
  129. }
  130. .pickerStyle(.segmented)
  131. } header: {
  132. Text("Variability")
  133. } footer: {
  134. Text("Std Dev — Standard Deviation, how much glucose swings around the average. CV — Coefficient of Variation, that swing relative to the average (Std Dev ÷ mean).")
  135. }
  136. }
  137. }
  138. }
  139. /// Standalone page for units configuration during onboarding.
  140. /// Shows a checkmark button in the toolbar to complete setup.
  141. struct UnitsOnboardingView: View {
  142. let onComplete: () -> Void
  143. var body: some View {
  144. Form {
  145. UnitsConfigurationView()
  146. }
  147. .navigationTitle("Set Up Units")
  148. .navigationBarTitleDisplayMode(.inline)
  149. .toolbar {
  150. ToolbarItem(placement: .navigationBarTrailing) {
  151. Button(action: {
  152. Storage.shared.hasConfiguredUnits.value = true
  153. onComplete()
  154. }) {
  155. Image(systemName: "checkmark")
  156. .fontWeight(.semibold)
  157. }
  158. }
  159. }
  160. }
  161. }