QuantityPicker.swift 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. //
  2. // QuantityPicker.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/23/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import LoopKit
  11. private struct PickerValueBoundsKey: PreferenceKey {
  12. static let defaultValue: [Anchor<CGRect>] = []
  13. static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) {
  14. value.append(contentsOf: nextValue())
  15. }
  16. }
  17. public struct QuantityPicker: View {
  18. @Binding var value: HKQuantity
  19. var unit: HKUnit
  20. var isUnitLabelVisible: Bool
  21. var colorForValue: (_ value: Double) -> Color
  22. private let selectableValues: [Double]
  23. private let formatter: NumberFormatter
  24. private let unitLabelSpacing: CGFloat = -6
  25. public init(
  26. value: Binding<HKQuantity>,
  27. unit: HKUnit,
  28. stride: HKQuantity,
  29. guardrail: Guardrail<HKQuantity>,
  30. formatter: NumberFormatter? = nil,
  31. isUnitLabelVisible: Bool = true,
  32. guidanceColors: GuidanceColors = GuidanceColors()
  33. ) {
  34. let selectableValues = guardrail.allValues(stridingBy: stride, unit: unit)
  35. self.init(value: value,
  36. unit: unit,
  37. guardrail: guardrail,
  38. selectableValues: selectableValues,
  39. formatter: formatter,
  40. isUnitLabelVisible: isUnitLabelVisible,
  41. guidanceColors: guidanceColors)
  42. }
  43. public init(
  44. value: Binding<HKQuantity>,
  45. unit: HKUnit,
  46. guardrail: Guardrail<HKQuantity>,
  47. selectableValues: [Double],
  48. formatter: NumberFormatter? = nil,
  49. isUnitLabelVisible: Bool = true,
  50. guidanceColors: GuidanceColors
  51. ) {
  52. self.init(
  53. value: value,
  54. unit: unit,
  55. selectableValues: selectableValues.map { unit.roundForPicker(value: $0) },
  56. formatter: formatter,
  57. isUnitLabelVisible: isUnitLabelVisible,
  58. colorForValue: { value in
  59. let quantity = HKQuantity(unit: unit, doubleValue: value)
  60. return guardrail.color(for: quantity, guidanceColors: guidanceColors)
  61. }
  62. )
  63. }
  64. public init(
  65. value: Binding<HKQuantity>,
  66. unit: HKUnit,
  67. selectableValues: [Double],
  68. formatter: NumberFormatter? = nil,
  69. isUnitLabelVisible: Bool = true,
  70. colorForValue: @escaping (_ value: Double) -> Color = { _ in .primary }
  71. ) {
  72. self._value = value
  73. self.unit = unit
  74. self.selectableValues = selectableValues
  75. self.formatter = formatter ?? {
  76. let quantityFormatter = QuantityFormatter()
  77. quantityFormatter.setPreferredNumberFormatter(for: unit)
  78. return quantityFormatter.numberFormatter
  79. }()
  80. self.isUnitLabelVisible = isUnitLabelVisible
  81. self.colorForValue = colorForValue
  82. }
  83. private var selectedValue: Binding<Double> {
  84. Binding(
  85. get: {
  86. unit.roundForPicker(value: value.doubleValue(for: unit))
  87. },
  88. set: { newValue in
  89. self.value = HKQuantity(unit: unit, doubleValue: newValue)
  90. }
  91. )
  92. }
  93. public var body: some View {
  94. picker
  95. .labelsHidden()
  96. .pickerStyle(.wheel)
  97. .overlayPreferenceValue(PickerValueBoundsKey.self, unitLabel(positionedFrom:))
  98. .accessibility(identifier: "quantity_picker")
  99. }
  100. @ViewBuilder
  101. private var picker: some View {
  102. // NOTE: iOS 15.1 introduced an issue where SwiftUI Pickers would not obey the `.clipped()`
  103. // directive when it comes to touchable area. I have submitted a bug (Feedback) to Apple (FB9788944).
  104. // This uses a custom Picker that works around the issue, but not perfectly (it isn't a 1 to 1 match).
  105. // If they ever do fix this, consider restoring the code from the commit prior to this change.
  106. // See LOOP-3870 for more details.
  107. ResizeablePicker(selection: selectedValue,
  108. data: selectableValues,
  109. formatter: { self.formatter.string(from: $0) ?? "\($0)" },
  110. colorer: colorForValue)
  111. .anchorPreference(key: PickerValueBoundsKey.self, value: .bounds, transform: { [$0] })
  112. }
  113. private func unitLabel(positionedFrom pickerValueBounds: [Anchor<CGRect>]) -> some View {
  114. GeometryReader { geometry in
  115. if self.isUnitLabelVisible && !pickerValueBounds.isEmpty {
  116. Text(self.unit.shortLocalizedUnitString())
  117. .foregroundColor(.gray)
  118. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
  119. .offset(x: pickerValueBounds.union(in: geometry).maxX + unitLabelSpacing)
  120. .animation(.default)
  121. }
  122. }
  123. }
  124. }
  125. extension Sequence where Element == Anchor<CGRect> {
  126. func union(in geometry: GeometryProxy) -> CGRect {
  127. lazy
  128. .map { geometry[$0] }
  129. .reduce(.null) { $0.union($1) }
  130. }
  131. }