GuardrailConstraintedQuantityView.swift 3.7 KB

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