GlucoseRangePicker.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. //
  2. // GlucoseRangePicker.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 5/14/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import LoopKit
  11. public struct GlucoseRangePicker: View {
  12. public enum UsageContext: Equatable {
  13. /// This picker is one component of a larger multi-component picker (e.g. a schedule item picker).
  14. case component(availableWidth: CGFloat)
  15. /// This picker operates independently.
  16. case independent
  17. }
  18. @Binding var lowerBound: HKQuantity
  19. @Binding var upperBound: HKQuantity
  20. var unit: HKUnit
  21. var minValue: HKQuantity?
  22. var maxValue: HKQuantity?
  23. var guardrail: Guardrail<HKQuantity>
  24. var formatter: NumberFormatter
  25. var usageContext: UsageContext
  26. public init(
  27. range: Binding<ClosedRange<HKQuantity>>,
  28. unit: HKUnit,
  29. minValue: HKQuantity?,
  30. maxValue: HKQuantity? = nil,
  31. guardrail: Guardrail<HKQuantity>,
  32. usageContext: UsageContext = .independent
  33. ) {
  34. self._lowerBound = Binding(
  35. get: { range.wrappedValue.lowerBound },
  36. set: {
  37. if $0 > range.wrappedValue.upperBound {
  38. // Prevent crash if picker gets into state where "lower bound" > "upper bound"
  39. range.wrappedValue = $0...$0
  40. }
  41. range.wrappedValue = $0...range.wrappedValue.upperBound
  42. }
  43. )
  44. self._upperBound = Binding(
  45. get: { range.wrappedValue.upperBound },
  46. set: {
  47. if range.wrappedValue.lowerBound > $0 {
  48. // Prevent crash if picker gets into state where "lower bound" > "upper bound"
  49. range.wrappedValue = range.wrappedValue.lowerBound...range.wrappedValue.lowerBound
  50. } else {
  51. range.wrappedValue = range.wrappedValue.lowerBound...$0
  52. }
  53. }
  54. )
  55. self.unit = unit
  56. self.minValue = minValue
  57. self.maxValue = maxValue
  58. self.guardrail = guardrail
  59. self.formatter = {
  60. let quantityFormatter = QuantityFormatter()
  61. quantityFormatter.setPreferredNumberFormatter(for: unit)
  62. return quantityFormatter.numberFormatter
  63. }()
  64. self.usageContext = usageContext
  65. }
  66. public var body: some View {
  67. switch usageContext {
  68. case .component(availableWidth: let availableWidth):
  69. return AnyView(body(availableWidth: availableWidth))
  70. case .independent:
  71. return AnyView(
  72. GeometryReader { geometry in
  73. HStack(spacing: 0) {
  74. Spacer()
  75. self.body(availableWidth: geometry.size.width)
  76. Spacer()
  77. }
  78. }
  79. .frame(height: 216)
  80. )
  81. }
  82. }
  83. private func body(availableWidth: CGFloat) -> some View {
  84. HStack(spacing: 0) {
  85. GlucoseValuePicker(
  86. value: $lowerBound,
  87. unit: unit,
  88. guardrail: guardrail,
  89. bounds: lowerBoundRange,
  90. isUnitLabelVisible: false
  91. )
  92. // Ensure the selectable picker values update when either bound changes
  93. .id(lowerBound...upperBound)
  94. .frame(width: availableWidth / 3.5)
  95. .overlay(
  96. Text(separator)
  97. .foregroundColor(Color(.secondaryLabel))
  98. .offset(x: spacing + separatorWidth),
  99. alignment: .trailing
  100. )
  101. .padding(.leading, usageContext == .independent ? unitLabelWidth : 0)
  102. .padding(.trailing, spacing + separatorWidth + spacing)
  103. .clipped()
  104. .accessibility(identifier: "min_glucose_picker")
  105. GlucoseValuePicker(
  106. value: $upperBound,
  107. unit: unit,
  108. guardrail: guardrail,
  109. bounds: upperBoundRange
  110. )
  111. // Ensure the selectable picker values update when either bound changes
  112. .id(lowerBound...upperBound)
  113. .frame(width: availableWidth / 3.5)
  114. .padding(.trailing, unitLabelWidth)
  115. .clipped()
  116. .accessibility(identifier: "max_glucose_picker")
  117. }
  118. }
  119. var separator: String { "–" }
  120. var separatorWidth: CGFloat {
  121. let attributedSeparator = NSAttributedString(
  122. string: separator,
  123. attributes: [.font: UIFont.preferredFont(forTextStyle: .body)]
  124. )
  125. return attributedSeparator.size().width
  126. }
  127. var spacing: CGFloat { 8 }
  128. var unitLabelWidth: CGFloat {
  129. let attributedUnitString = NSAttributedString(
  130. string: unit.shortLocalizedUnitString(),
  131. attributes: [.font: UIFont.preferredFont(forTextStyle: .body)]
  132. )
  133. return attributedUnitString.size().width
  134. }
  135. var lowerBoundRange: ClosedRange<HKQuantity> {
  136. let min = minValue.map { Swift.max(guardrail.absoluteBounds.lowerBound, $0) }
  137. ?? guardrail.absoluteBounds.lowerBound
  138. let max = Swift.min(guardrail.absoluteBounds.upperBound, upperBound)
  139. return min...max
  140. }
  141. var upperBoundRange: ClosedRange<HKQuantity> {
  142. let min = max(guardrail.absoluteBounds.lowerBound, lowerBound)
  143. let max = maxValue.map { Swift.min(guardrail.absoluteBounds.upperBound, $0) }
  144. ?? guardrail.absoluteBounds.upperBound
  145. return min...max
  146. }
  147. }