GlucoseRangePicker.swift 5.2 KB

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