GlucoseTherapySettingInformationView.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //
  2. // GlucoseTherapySettingInformationView.swift
  3. // LoopKitUI
  4. //
  5. // Created by Rick Pasetto on 11/16/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. import SwiftUI
  10. import LoopKit
  11. public struct GlucoseTherapySettingInformationView<Content: View>: View {
  12. var text: Content?
  13. let onExit: (() -> Void)?
  14. let mode: SettingsPresentationMode
  15. let therapySetting: TherapySetting
  16. let preferredUnit: HKUnit
  17. let appName: String
  18. @Environment(\.presentationMode) var presentationMode
  19. public init(
  20. therapySetting: TherapySetting,
  21. preferredUnit: HKUnit? = nil,
  22. onExit: (() -> Void)?,
  23. mode: SettingsPresentationMode = .acceptanceFlow,
  24. appName: String,
  25. text: Content? = nil
  26. ){
  27. self.therapySetting = therapySetting
  28. self.preferredUnit = preferredUnit ?? .milligramsPerDeciliter
  29. self.onExit = onExit
  30. self.mode = mode
  31. self.appName = appName
  32. self.text = text
  33. }
  34. public init(
  35. therapySetting: TherapySetting,
  36. preferredUnit: HKUnit? = nil,
  37. onExit: (() -> Void)?,
  38. mode: SettingsPresentationMode = .acceptanceFlow,
  39. appName: String,
  40. text: Content? = nil
  41. ) where Content == EmptyView {
  42. self.therapySetting = therapySetting
  43. self.preferredUnit = preferredUnit ?? .milligramsPerDeciliter
  44. self.onExit = onExit
  45. self.mode = mode
  46. self.appName = appName
  47. self.text = text
  48. }
  49. public var body: some View {
  50. InformationView(
  51. title: Text(self.therapySetting.title),
  52. informationalContent: {
  53. illustration
  54. bodyText
  55. },
  56. onExit: onExit ?? { self.presentationMode.wrappedValue.dismiss() },
  57. mode: mode
  58. )
  59. }
  60. private var illustration: some View {
  61. Image(frameworkImage: illustrationImageName)
  62. .renderingMode(.original)
  63. .resizable()
  64. .aspectRatio(contentMode: ContentMode.fit)
  65. }
  66. private var bodyText: some View {
  67. VStack(alignment: .leading, spacing: 25) {
  68. if let text {
  69. text
  70. } else {
  71. Text(therapySetting.descriptiveText(appName: appName))
  72. }
  73. Text(therapySetting.guardrailInformationText)
  74. }
  75. .fixedSize(horizontal: false, vertical: true)
  76. }
  77. private var illustrationImageName: String {
  78. return "\(therapySetting) \(preferredUnit.description.replacingOccurrences(of: "/", with: ""))"
  79. }
  80. }
  81. fileprivate let mgdLFormatter = QuantityFormatter(for: .milligramsPerDeciliter)
  82. fileprivate let mmolLFormatter = QuantityFormatter(for: .millimolesPerLiter)
  83. fileprivate extension TherapySetting {
  84. // TODO: pass in preferredUnit instead of having both units.
  85. var guardrailInformationText: String {
  86. switch self {
  87. case .glucoseTargetRange:
  88. return lowHighText(for: Guardrail.correctionRange)
  89. case .preMealCorrectionRangeOverride:
  90. let mgdlMax = Guardrail.premealCorrectionRangeMaximum.doubleValue(
  91. for: .milligramsPerDeciliter,
  92. withRounding: true,
  93. rule: .down
  94. )
  95. let mmolMax = Guardrail.premealCorrectionRangeMaximum.doubleValue(
  96. for: .millimolesPerLiter,
  97. withRounding: true,
  98. rule: .down
  99. )
  100. let upperBoundString = bothUnitsString(
  101. mgdlValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdlMax),
  102. mmolValue: HKQuantity(unit: .millimolesPerLiter, doubleValue: mmolMax)
  103. )
  104. return lowHighText(lowerBoundString: LocalizedString("your Glucose Safety Limit", comment: "Lower bound pre-meal information text"),
  105. upperBoundString: upperBoundString)
  106. case .workoutCorrectionRangeOverride:
  107. let mgdlMin = Guardrail.unconstrainedWorkoutCorrectionRange.absoluteBounds.lowerBound.doubleValue(
  108. for: .milligramsPerDeciliter,
  109. withRounding: true,
  110. rule: .down
  111. )
  112. let mmolMin = Guardrail.unconstrainedWorkoutCorrectionRange.absoluteBounds.lowerBound.doubleValue(
  113. for: .millimolesPerLiter,
  114. withRounding: true,
  115. rule: .down
  116. )
  117. let lowerBoundString = bothUnitsString(
  118. mgdlValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdlMin),
  119. mmolValue: HKQuantity(unit: .millimolesPerLiter, doubleValue: mmolMin)
  120. )
  121. let mgdlMax = Guardrail.unconstrainedWorkoutCorrectionRange.absoluteBounds.upperBound.doubleValue(
  122. for: .milligramsPerDeciliter,
  123. withRounding: true,
  124. rule: .down
  125. )
  126. let mmolMax = Guardrail.unconstrainedWorkoutCorrectionRange.absoluteBounds.upperBound.doubleValue(
  127. for: .millimolesPerLiter,
  128. withRounding: true,
  129. rule: .down
  130. )
  131. let upperBoundString = bothUnitsString(
  132. mgdlValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdlMax),
  133. mmolValue: HKQuantity(unit: .millimolesPerLiter, doubleValue: mmolMax)
  134. )
  135. return lowHighText(
  136. lowerBoundString: String(format: LocalizedString("%1$@ or your Glucose Safety Limit, whichever is higher", comment: "Lower bound workout information text format (1: app name)"), lowerBoundString),
  137. upperBoundString: upperBoundString)
  138. case .suspendThreshold:
  139. return lowHighText(for: Guardrail.suspendThreshold)
  140. case .basalRate, .deliveryLimits, .insulinModel, .carbRatio, .insulinSensitivity, .none:
  141. fatalError("Unexpected")
  142. }
  143. }
  144. func bothUnitsString(mgdlValue: HKQuantity, mmolValue: HKQuantity) -> String {
  145. String(format: "%1$@ (%2$@)",
  146. mgdLFormatter.string(from: mgdlValue)!,
  147. mmolLFormatter.string(from: mmolValue)!)
  148. }
  149. func lowHighText(for guardrail: Guardrail<HKQuantity>) -> String {
  150. let mgdlValues = guardrail.absoluteBounds.roundedDisplayValues(for: .milligramsPerDeciliter)
  151. let mmolValues = guardrail.absoluteBounds.roundedDisplayValues(for: .millimolesPerLiter)
  152. let lowerBoundString = bothUnitsString(
  153. mgdlValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdlValues.first!),
  154. mmolValue: HKQuantity(unit: .millimolesPerLiter, doubleValue: mmolValues.first!)
  155. )
  156. let upperBoundString = bothUnitsString(
  157. mgdlValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdlValues.last!),
  158. mmolValue: HKQuantity(unit: .millimolesPerLiter, doubleValue: mmolValues.last!)
  159. )
  160. return lowHighText(lowerBoundString: lowerBoundString, upperBoundString: upperBoundString)
  161. }
  162. func lowHighText(lowerBoundString: String, upperBoundString: String) -> String {
  163. return String(format: LocalizedString("It can be set as low as %1$@. It can be set as high as %2$@.",
  164. comment: "Guardrail info text format"), lowerBoundString, upperBoundString)
  165. }
  166. }