SuspendThresholdEditor.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. //
  2. // SuspendThresholdEditor.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/10/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import LoopKit
  11. public struct SuspendThresholdEditor: View {
  12. var initialValue: HKQuantity?
  13. var unit: HKUnit
  14. var maxValue: HKQuantity?
  15. var save: (_ suspendThreshold: HKQuantity) -> Void
  16. let mode: SettingsPresentationMode
  17. @State private var userDidTap: Bool = false
  18. @State var value: HKQuantity
  19. @State var isEditing = false
  20. @State var showingConfirmationAlert = false
  21. @Environment(\.dismiss) var dismiss
  22. @Environment(\.authenticate) var authenticate
  23. @Environment(\.appName) private var appName
  24. let guardrail = Guardrail.suspendThreshold
  25. public init(
  26. value: HKQuantity?,
  27. unit: HKUnit,
  28. maxValue: HKQuantity?,
  29. onSave save: @escaping (_ suspendThreshold: HKQuantity) -> Void,
  30. mode: SettingsPresentationMode = .settings
  31. ) {
  32. self._value = State(initialValue: value ?? Self.defaultValue(for: unit))
  33. self.initialValue = value
  34. self.unit = unit
  35. self.maxValue = maxValue
  36. self.save = save
  37. self.mode = mode
  38. }
  39. public init(
  40. viewModel: TherapySettingsViewModel,
  41. didSave: (() -> Void)? = nil
  42. ) {
  43. let unit = viewModel.therapySettings.glucoseUnit ?? viewModel.preferredGlucoseUnit
  44. self.init(
  45. value: viewModel.therapySettings.suspendThreshold?.quantity,
  46. unit: unit,
  47. maxValue: Guardrail.maxSuspendThresholdValue(
  48. correctionRangeSchedule: viewModel.therapySettings.glucoseTargetRangeSchedule,
  49. preMealTargetRange: viewModel.therapySettings.preMealTargetRange?.quantityRange(for: unit),
  50. workoutTargetRange: viewModel.therapySettings.workoutTargetRange?.quantityRange(for: unit)
  51. ),
  52. onSave: { [weak viewModel] newValue in
  53. guard let viewModel = viewModel else {
  54. return
  55. }
  56. let newThreshold = GlucoseThreshold(unit: viewModel.preferredGlucoseUnit, value: newValue.doubleValue(for: viewModel.preferredGlucoseUnit))
  57. viewModel.saveSuspendThreshold(value: newThreshold)
  58. didSave?()
  59. },
  60. mode: viewModel.mode
  61. )
  62. }
  63. private static func defaultValue(for unit: HKUnit) -> HKQuantity {
  64. switch unit {
  65. case .milligramsPerDeciliter:
  66. return HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)
  67. case .millimolesPerLiter:
  68. return HKQuantity(unit: .millimolesPerLiter, doubleValue: 4.5)
  69. default:
  70. fatalError("Unsupported glucose unit \(unit)")
  71. }
  72. }
  73. public var body: some View {
  74. switch mode {
  75. case .settings: return AnyView(contentWithCancel)
  76. case .acceptanceFlow: return AnyView(content)
  77. }
  78. }
  79. private var contentWithCancel: some View {
  80. if value == initialValue {
  81. return AnyView(content
  82. .navigationBarBackButtonHidden(false)
  83. .navigationBarItems(leading: EmptyView())
  84. )
  85. } else {
  86. return AnyView(content
  87. .navigationBarBackButtonHidden(true)
  88. .navigationBarItems(leading: cancelButton)
  89. )
  90. }
  91. }
  92. private var cancelButton: some View {
  93. Button(action: { self.dismiss() } ) { Text(LocalizedString("Cancel", comment: "Cancel editing settings button title")) }
  94. }
  95. private var content: some View {
  96. ConfigurationPage(
  97. title: Text(TherapySetting.suspendThreshold.title),
  98. actionButtonTitle: Text(mode.buttonText),
  99. actionButtonState: saveButtonState,
  100. cards: {
  101. // TODO: Remove conditional when Swift 5.3 ships
  102. // https://bugs.swift.org/browse/SR-11628
  103. if true {
  104. Card {
  105. SettingDescription(text: description, informationalContent: { TherapySetting.suspendThreshold.helpScreen() })
  106. ExpandableSetting(
  107. isEditing: $isEditing,
  108. valueContent: {
  109. GuardrailConstrainedQuantityView(
  110. value: value,
  111. unit: unit,
  112. guardrail: guardrail,
  113. isEditing: isEditing,
  114. // Workaround for strange animation behavior on appearance
  115. forceDisableAnimations: true
  116. )
  117. },
  118. expandedContent: {
  119. GlucoseValuePicker(
  120. value: self.$value.animation(),
  121. unit: self.unit,
  122. guardrail: self.guardrail,
  123. bounds: self.guardrail.absoluteBounds.lowerBound...(self.maxValue ?? self.guardrail.absoluteBounds.upperBound)
  124. )
  125. // Prevent the picker from expanding the card's width on small devices
  126. .frame(maxWidth: UIScreen.main.bounds.width - 48)
  127. .clipped()
  128. }
  129. )
  130. }
  131. }
  132. },
  133. actionAreaContent: {
  134. instructionalContentIfNecessary
  135. if warningThreshold != nil && (userDidTap || mode != .acceptanceFlow) {
  136. SuspendThresholdGuardrailWarning(safetyClassificationThreshold: warningThreshold!)
  137. }
  138. },
  139. action: {
  140. if self.warningThreshold == nil {
  141. self.startSaving()
  142. } else {
  143. self.showingConfirmationAlert = true
  144. }
  145. }
  146. )
  147. .alert(isPresented: $showingConfirmationAlert, content: confirmationAlert)
  148. .navigationBarTitle("", displayMode: .inline)
  149. .onTapGesture {
  150. self.userDidTap = true
  151. }
  152. }
  153. var description: Text {
  154. Text(TherapySetting.suspendThreshold.descriptiveText(appName: appName))
  155. }
  156. private var instructionalContentIfNecessary: some View {
  157. return Group {
  158. if mode == .acceptanceFlow && !userDidTap {
  159. instructionalContent
  160. }
  161. }
  162. }
  163. private var instructionalContent: some View {
  164. HStack { // to align with guardrail warning, if present
  165. Text(LocalizedString("You can edit the setting by tapping into the line item.", comment: "Description of how to edit setting"))
  166. .foregroundColor(.secondary)
  167. .font(.subheadline)
  168. Spacer()
  169. }
  170. }
  171. private var saveButtonState: ConfigurationPageActionButtonState {
  172. initialValue == nil || value != initialValue! || mode == .acceptanceFlow ? .enabled : .disabled
  173. }
  174. private var warningThreshold: SafetyClassification.Threshold? {
  175. switch guardrail.classification(for: value) {
  176. case .withinRecommendedRange:
  177. return nil
  178. case .outsideRecommendedRange(let threshold):
  179. return threshold
  180. }
  181. }
  182. private func confirmationAlert() -> SwiftUI.Alert {
  183. SwiftUI.Alert(
  184. title: Text(LocalizedString("Save Glucose Safety Limit?", comment: "Alert title for confirming a glucose safety limit outside the recommended range")),
  185. message: Text(TherapySetting.suspendThreshold.guardrailSaveWarningCaption),
  186. primaryButton: .cancel(Text(LocalizedString("Go Back", comment: "Text for go back action on confirmation alert"))),
  187. secondaryButton: .default(
  188. Text(LocalizedString("Continue", comment: "Text for continue action on confirmation alert")),
  189. action: startSaving
  190. )
  191. )
  192. }
  193. private func startSaving() {
  194. guard mode == .settings else {
  195. self.continueSaving()
  196. return
  197. }
  198. authenticate(TherapySetting.suspendThreshold.authenticationChallengeDescription) {
  199. switch $0 {
  200. case .success: self.continueSaving()
  201. case .failure: break
  202. }
  203. }
  204. }
  205. private func continueSaving() {
  206. self.save(self.value)
  207. }
  208. }
  209. struct SuspendThresholdGuardrailWarning: View {
  210. var safetyClassificationThreshold: SafetyClassification.Threshold
  211. var body: some View {
  212. GuardrailWarning(title: title, threshold: safetyClassificationThreshold)
  213. }
  214. private var title: Text {
  215. switch safetyClassificationThreshold {
  216. case .minimum, .belowRecommended:
  217. return Text(LocalizedString("Low Glucose Safety Limit", comment: "Title text for the low glucose safety limit warning"))
  218. case .aboveRecommended, .maximum:
  219. return Text(LocalizedString("High Glucose Safety Limit", comment: "Title text for the high glucose safety limit warning"))
  220. }
  221. }
  222. }
  223. struct SuspendThresholdView_Previews: PreviewProvider {
  224. static var previews: some View {
  225. SuspendThresholdEditor(value: nil, unit: .milligramsPerDeciliter, maxValue: nil, onSave: { _ in })
  226. }
  227. }