GlucoseRangePicker.swift 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. body(availableWidth: availableWidth)
  70. case .independent:
  71. GeometryReader { geometry in
  72. HStack(spacing: 0) {
  73. Spacer()
  74. self.body(availableWidth: geometry.size.width)
  75. Spacer()
  76. }
  77. }
  78. .frame(height: 216)
  79. }
  80. }
  81. private func body(availableWidth: CGFloat) -> some View {
  82. HStack(spacing: 0) {
  83. GlucoseValuePicker(
  84. value: $lowerBound,
  85. unit: unit,
  86. guardrail: guardrail,
  87. bounds: lowerBoundRange,
  88. isUnitLabelVisible: false
  89. )
  90. .frame(width: availableWidth / 3)
  91. .overlay(
  92. Text(separator)
  93. .foregroundColor(Color(.secondaryLabel))
  94. .offset(x: spacing + separatorWidth),
  95. alignment: .trailing
  96. )
  97. .padding(.leading, usageContext == .independent ? unitLabelWidth : 0)
  98. .padding(.trailing, spacing + separatorWidth + spacing)
  99. .clipped()
  100. .compositingGroup()
  101. .accessibility(identifier: "min_glucose_picker")
  102. GlucoseValuePicker(
  103. value: $upperBound,
  104. unit: unit,
  105. guardrail: guardrail,
  106. bounds: upperBoundRange
  107. )
  108. .frame(width: availableWidth / 3)
  109. .padding(.trailing, unitLabelWidth)
  110. .clipped()
  111. .compositingGroup()
  112. .accessibility(identifier: "max_glucose_picker")
  113. }
  114. }
  115. var separator: String { "–" }
  116. var separatorWidth: CGFloat {
  117. let attributedSeparator = NSAttributedString(
  118. string: separator,
  119. attributes: [.font: UIFont.preferredFont(forTextStyle: .body)]
  120. )
  121. return attributedSeparator.size().width
  122. }
  123. var spacing: CGFloat { 4 }
  124. var unitLabelWidth: CGFloat {
  125. let attributedUnitString = NSAttributedString(
  126. string: unit.shortLocalizedUnitString(),
  127. attributes: [.font: UIFont.preferredFont(forTextStyle: .body)]
  128. )
  129. return attributedUnitString.size().width
  130. }
  131. var lowerBoundRange: ClosedRange<HKQuantity> {
  132. let min = minValue.map { Swift.max(guardrail.absoluteBounds.lowerBound, $0) }
  133. ?? guardrail.absoluteBounds.lowerBound
  134. let max = Swift.min(guardrail.absoluteBounds.upperBound, upperBound)
  135. return min...max
  136. }
  137. var upperBoundRange: ClosedRange<HKQuantity> {
  138. let min = max(guardrail.absoluteBounds.lowerBound, lowerBound)
  139. let max = maxValue.map { Swift.min(guardrail.absoluteBounds.upperBound, $0) }
  140. ?? guardrail.absoluteBounds.upperBound
  141. return min...max
  142. }
  143. }