AlarmThresholdPickerSection.swift 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. //
  2. // AlarmThresholdPickerSection.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-05-06.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. struct AlarmThresholdRow: View {
  11. // ── Public API ──────────────────────────────────────────────────────────────
  12. let title: String
  13. let range: ClosedRange<Double>
  14. let step: Double // 1 for mg/dL, 0.1 for mmol/L
  15. @Binding var value: Double // **stored in mg/dL**
  16. // ── Private state ──────────────────────────────────────────────────────────
  17. @State private var showPicker = false
  18. // Preferred unit – mmol/L or mg/dL
  19. private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() }
  20. private var displayValue: String {
  21. format(value, in: unit)
  22. }
  23. // Generate all selectable display values for the picker
  24. private var pickerValues: [Double] {
  25. stride(from: range.lowerBound, through: range.upperBound, by: step)
  26. .map { $0 }
  27. }
  28. var body: some View {
  29. Section {
  30. // Collapsed row
  31. HStack {
  32. Text(title)
  33. Spacer()
  34. Text(displayValue)
  35. .foregroundColor(.secondary)
  36. }
  37. .contentShape(Rectangle())
  38. .onTapGesture { showPicker = true }
  39. }
  40. .sheet(isPresented: $showPicker) {
  41. // Expanded wheel picker
  42. NavigationStack {
  43. VStack {
  44. Picker("", selection: bindingForPicker) {
  45. ForEach(pickerValues, id: \.self) { v in
  46. Text(format(v, in: unit))
  47. .tag(v)
  48. }
  49. }
  50. .pickerStyle(.wheel)
  51. }
  52. .navigationTitle(title)
  53. .toolbar {
  54. ToolbarItem(placement: .confirmationAction) {
  55. Button("Done") { showPicker = false }
  56. }
  57. }
  58. } .presentationDetents([.fraction(0.35), .medium]) // 35 % or medium height
  59. .presentationDragIndicator(.visible) // the little grab-bar
  60. }
  61. }
  62. // MARK: – Helpers
  63. private func format(_ mgdl: Double, in unit: HKUnit) -> String {
  64. if unit == .millimolesPerLiter {
  65. let mmol = mgdl / 18.0
  66. return String(format: "%.1f mmol/L", mmol)
  67. } else {
  68. return String(format: "%.0f mg/dL", mgdl)
  69. }
  70. }
  71. // Bind the picker directly to `value`, snapping to the closest choice
  72. private var bindingForPicker: Binding<Double> {
  73. Binding(
  74. get: {
  75. // pick the closest representable value
  76. pickerValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value
  77. },
  78. set: { newVal in
  79. value = newVal // stored in mg/dL
  80. }
  81. )
  82. }
  83. }