QuantityPicker.swift 4.9 KB

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