QuantityPicker.swift 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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. public init(
  25. value: Binding<HKQuantity>,
  26. unit: HKUnit,
  27. stride: HKQuantity,
  28. guardrail: Guardrail<HKQuantity>,
  29. formatter: NumberFormatter? = nil,
  30. isUnitLabelVisible: Bool = true,
  31. guidanceColors: GuidanceColors = GuidanceColors()
  32. ) {
  33. let selectableValues = guardrail.allValues(stridingBy: stride, unit: 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,
  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()
  76. quantityFormatter.setPreferredNumberFormatter(for: unit)
  77. return quantityFormatter.numberFormatter
  78. }()
  79. self.isUnitLabelVisible = isUnitLabelVisible
  80. self.colorForValue = colorForValue
  81. }
  82. public var body: some View {
  83. Picker("Quantity", selection: $value.doubleValue(for: unit)) {
  84. ForEach(selectableValues, id: \.self) { value in
  85. Text(self.formatter.string(from: value) ?? "\(value)")
  86. .foregroundColor(self.colorForValue(value))
  87. .anchorPreference(key: PickerValueBoundsKey.self, value: .bounds, transform: { [$0] })
  88. .accessibility(identifier: self.formatter.string(from: value) ?? "\(value)")
  89. }
  90. }
  91. .labelsHidden()
  92. .pickerStyle(WheelPickerStyle())
  93. .overlayPreferenceValue(PickerValueBoundsKey.self, unitLabel(positionedFrom:))
  94. .accessibility(identifier: "quantity_picker")
  95. }
  96. private func unitLabel(positionedFrom pickerValueBounds: [Anchor<CGRect>]) -> some View {
  97. GeometryReader { geometry in
  98. if self.isUnitLabelVisible && !pickerValueBounds.isEmpty {
  99. Text(self.unit.shortLocalizedUnitString())
  100. .foregroundColor(.gray)
  101. .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
  102. .offset(x: pickerValueBounds.union(in: geometry).maxX + self.unitLabelSpacing)
  103. .animation(.default)
  104. }
  105. }
  106. }
  107. private var unitLabelSpacing: CGFloat { 8 }
  108. }
  109. extension Sequence where Element == Anchor<CGRect> {
  110. func union(in geometry: GeometryProxy) -> CGRect {
  111. lazy
  112. .map { geometry[$0] }
  113. .reduce(.null) { $0.union($1) }
  114. }
  115. }