GuardrailConstraintedQuantityView.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. //
  2. // GuardrailConstraintedQuantityView.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/24/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import LoopKit
  11. public struct GuardrailConstrainedQuantityView: View {
  12. @Environment(\.guidanceColors) var guidanceColors
  13. var value: HKQuantity?
  14. var unit: HKUnit
  15. var guardrail: Guardrail<HKQuantity>
  16. var isEditing: Bool
  17. var isSupportedValue: Bool
  18. var formatter: NumberFormatter
  19. var iconSpacing: CGFloat
  20. var isUnitLabelVisible: Bool
  21. var forceDisableAnimations: Bool
  22. @State private var hasAppeared = false
  23. public init(
  24. value: HKQuantity?,
  25. unit: HKUnit,
  26. guardrail: Guardrail<HKQuantity>,
  27. isEditing: Bool,
  28. isSupportedValue: Bool = true,
  29. iconSpacing: CGFloat = 8,
  30. isUnitLabelVisible: Bool = true,
  31. forceDisableAnimations: Bool = false
  32. ) {
  33. self.value = value
  34. self.unit = unit
  35. self.guardrail = guardrail
  36. self.isEditing = isEditing
  37. self.isSupportedValue = isSupportedValue
  38. self.iconSpacing = iconSpacing
  39. self.formatter = {
  40. let quantityFormatter = QuantityFormatter(for: unit)
  41. return quantityFormatter.numberFormatter
  42. }()
  43. self.isUnitLabelVisible = isUnitLabelVisible
  44. self.forceDisableAnimations = forceDisableAnimations
  45. }
  46. public var body: some View {
  47. HStack {
  48. HStack(spacing: iconSpacing) {
  49. if value != nil {
  50. if guardrail.classification(for: value!) != .withinRecommendedRange {
  51. Image(systemName: "exclamationmark.triangle.fill")
  52. .foregroundColor(warningColor)
  53. .transition(.springInDisappear)
  54. }
  55. Text(formatter.string(from: value!.doubleValue(for: unit)) ?? "\(value!.doubleValue(for: unit))")
  56. .foregroundColor(warningColor)
  57. .fixedSize(horizontal: true, vertical: false)
  58. } else {
  59. Text("–")
  60. .foregroundColor(.secondary)
  61. }
  62. }
  63. if isUnitLabelVisible {
  64. Text(unit.shortLocalizedUnitString())
  65. .foregroundColor(Color(.secondaryLabel))
  66. }
  67. }
  68. .accessibilityElement(children: .combine)
  69. .onAppear { self.hasAppeared = true }
  70. .animation(animation)
  71. }
  72. private var animation: Animation? {
  73. // A conditional implicit animation seems to behave funky on first appearance.
  74. // Disable animations until the view has appeared.
  75. if forceDisableAnimations || !hasAppeared {
  76. return nil
  77. }
  78. // While editing, the text width is liable to change, which can cause a slow-feeling animation
  79. // of the guardrail warning icon. Disable animations while editing.
  80. return isEditing ? nil : .default
  81. }
  82. private var warningColor: Color {
  83. guard let value = value else {
  84. return .primary
  85. }
  86. guard isSupportedValue else { return guidanceColors.critical }
  87. switch guardrail.classification(for: value) {
  88. case .withinRecommendedRange:
  89. return isEditing ? .accentColor : guidanceColors.acceptable
  90. case .outsideRecommendedRange(let threshold):
  91. switch threshold {
  92. case .minimum, .maximum:
  93. return guidanceColors.critical
  94. case .belowRecommended, .aboveRecommended:
  95. return guidanceColors.warning
  96. }
  97. }
  98. }
  99. }
  100. fileprivate extension AnyTransition {
  101. static let springInDisappear = asymmetric(
  102. insertion: AnyTransition.scale.animation(.spring(dampingFraction: 0.5)),
  103. removal: .identity
  104. )
  105. }