SuspendThresholdEditor.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. // Also known as "Glucose Safety Limit"
  12. public struct SuspendThresholdEditor: View {
  13. @EnvironmentObject private var displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
  14. @Environment(\.dismissAction) var dismiss
  15. @Environment(\.authenticate) var authenticate
  16. @Environment(\.appName) private var appName
  17. let mode: SettingsPresentationMode
  18. let viewModel: SuspendThresholdEditorViewModel
  19. @State private var userDidTap: Bool = false
  20. @State var value: HKQuantity
  21. @State var isEditing = false
  22. @State var showingConfirmationAlert = false
  23. private var initialValue: HKQuantity? {
  24. viewModel.suspendThreshold
  25. }
  26. public init(
  27. mode: SettingsPresentationMode,
  28. therapySettingsViewModel: TherapySettingsViewModel,
  29. didSave: (() -> Void)? = nil
  30. ) {
  31. self.mode = mode
  32. let viewModel = SuspendThresholdEditorViewModel(therapySettingsViewModel: therapySettingsViewModel,
  33. mode: mode,
  34. didSave: didSave)
  35. self._value = State(initialValue: viewModel.suspendThreshold ?? Self.defaultValue(for: viewModel.suspendThresholdUnit))
  36. self.viewModel = viewModel
  37. }
  38. private static func defaultValue(for unit: HKUnit) -> HKQuantity {
  39. switch unit {
  40. case .milligramsPerDeciliter:
  41. return HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)
  42. case .millimolesPerLiter:
  43. return HKQuantity(unit: .millimolesPerLiter, doubleValue: 4.5)
  44. default:
  45. fatalError("Unsupported glucose unit \(unit)")
  46. }
  47. }
  48. public var body: some View {
  49. switch mode {
  50. case .acceptanceFlow:
  51. content
  52. case .settings:
  53. contentWithCancel
  54. .navigationBarTitle("", displayMode: .inline)
  55. }
  56. }
  57. private var contentWithCancel: some View {
  58. if value == initialValue {
  59. return AnyView(content
  60. .navigationBarBackButtonHidden(false)
  61. .navigationBarItems(leading: EmptyView())
  62. )
  63. } else {
  64. return AnyView(content
  65. .navigationBarBackButtonHidden(true)
  66. .navigationBarItems(leading: cancelButton)
  67. )
  68. }
  69. }
  70. private var cancelButton: some View {
  71. Button(action: { self.dismiss() } ) { Text(LocalizedString("Cancel", comment: "Cancel editing settings button title")) }
  72. }
  73. private var picker: GlucoseValuePicker {
  74. GlucoseValuePicker(
  75. value: self.$value.animation(),
  76. unit: displayGlucoseUnitObservable.displayGlucoseUnit,
  77. guardrail: viewModel.guardrail,
  78. bounds: viewModel.guardrail.absoluteBounds.lowerBound...viewModel.maxSuspendThresholdValue
  79. )
  80. }
  81. private var content: some View {
  82. ConfigurationPage(
  83. title: Text(TherapySetting.suspendThreshold.title),
  84. actionButtonTitle: Text(mode.buttonText()),
  85. actionButtonState: saveButtonState,
  86. cards: {
  87. Card {
  88. SettingDescription(text: description, informationalContent: { TherapySetting.suspendThreshold.helpScreen() })
  89. ExpandableSetting(
  90. isEditing: $isEditing,
  91. valueContent: {
  92. GuardrailConstrainedQuantityView(
  93. value: value,
  94. unit: displayGlucoseUnitObservable.displayGlucoseUnit,
  95. guardrail: viewModel.guardrail,
  96. isEditing: isEditing,
  97. // Workaround for strange animation behavior on appearance
  98. forceDisableAnimations: true
  99. )
  100. },
  101. expandedContent: {
  102. // Prevent the picker from expanding the card's width on small devices
  103. picker.frame(maxWidth: 200)
  104. }
  105. )
  106. }
  107. },
  108. actionAreaContent: {
  109. instructionalContentIfNecessary
  110. if warningThreshold != nil && (userDidTap || mode != .acceptanceFlow) {
  111. SuspendThresholdGuardrailWarning(safetyClassificationThreshold: warningThreshold!)
  112. }
  113. },
  114. action: {
  115. if self.warningThreshold == nil {
  116. self.startSaving()
  117. } else {
  118. self.showingConfirmationAlert = true
  119. }
  120. }
  121. )
  122. .alert(isPresented: $showingConfirmationAlert, content: confirmationAlert)
  123. .simultaneousGesture(TapGesture().onEnded {
  124. withAnimation {
  125. self.userDidTap = true
  126. }
  127. })
  128. }
  129. var description: Text {
  130. Text(TherapySetting.suspendThreshold.descriptiveText(appName: appName))
  131. }
  132. private var instructionalContentIfNecessary: some View {
  133. return Group {
  134. if mode == .acceptanceFlow && !userDidTap {
  135. instructionalContent
  136. }
  137. }
  138. }
  139. private var instructionalContent: some View {
  140. HStack { // to align with guardrail warning, if present
  141. Text(LocalizedString("You can edit the setting by tapping into the line item.", comment: "Description of how to edit setting"))
  142. .foregroundColor(.secondary)
  143. .font(.subheadline)
  144. Spacer()
  145. }
  146. }
  147. private var saveButtonState: ConfigurationPageActionButtonState {
  148. let selectableValues = picker.selectableValues
  149. let adjustedBounds = (selectableValues.first!)...(selectableValues.last!)
  150. guard adjustedBounds.contains(value.doubleValue(for: displayGlucoseUnitObservable.displayGlucoseUnit)) else {
  151. return .disabled
  152. }
  153. return initialValue == nil || value != initialValue || mode == .acceptanceFlow ? .enabled : .disabled
  154. }
  155. private var warningThreshold: SafetyClassification.Threshold? {
  156. switch viewModel.guardrail.classification(for: value) {
  157. case .withinRecommendedRange:
  158. return nil
  159. case .outsideRecommendedRange(let threshold):
  160. return threshold
  161. }
  162. }
  163. private func confirmationAlert() -> SwiftUI.Alert {
  164. SwiftUI.Alert(
  165. title: Text(LocalizedString("Save Glucose Safety Limit?", comment: "Alert title for confirming a glucose safety limit outside the recommended range")),
  166. message: Text(TherapySetting.suspendThreshold.guardrailSaveWarningCaption),
  167. primaryButton: .cancel(Text(LocalizedString("Go Back", comment: "Text for go back action on confirmation alert"))),
  168. secondaryButton: .default(
  169. Text(LocalizedString("Continue", comment: "Text for continue action on confirmation alert")),
  170. action: startSaving
  171. )
  172. )
  173. }
  174. private func startSaving() {
  175. guard mode == .settings else {
  176. self.continueSaving()
  177. return
  178. }
  179. authenticate(TherapySetting.suspendThreshold.authenticationChallengeDescription) {
  180. switch $0 {
  181. case .success: self.continueSaving()
  182. case .failure: break
  183. }
  184. }
  185. }
  186. private func continueSaving() {
  187. viewModel.saveSuspendThreshold(self.value, displayGlucoseUnitObservable.displayGlucoseUnit)
  188. }
  189. }
  190. struct SuspendThresholdGuardrailWarning: View {
  191. var safetyClassificationThreshold: SafetyClassification.Threshold
  192. var body: some View {
  193. GuardrailWarning(therapySetting: .suspendThreshold, title: title, threshold: safetyClassificationThreshold)
  194. }
  195. private var title: Text {
  196. switch safetyClassificationThreshold {
  197. case .minimum, .belowRecommended:
  198. return Text(LocalizedString("Low Glucose Safety Limit", comment: "Title text for the low glucose safety limit warning"))
  199. case .aboveRecommended, .maximum:
  200. return Text(LocalizedString("High Glucose Safety Limit", comment: "Title text for the high glucose safety limit warning"))
  201. }
  202. }
  203. }
  204. struct SuspendThresholdView_Previews: PreviewProvider {
  205. static var previews: some View {
  206. let therapySettingsViewModel = TherapySettingsViewModel(therapySettings: TherapySettings())
  207. return SuspendThresholdEditor(mode: .settings, therapySettingsViewModel: therapySettingsViewModel, didSave: nil)
  208. .environmentObject(DisplayGlucoseUnitObservable(displayGlucoseUnit: .milligramsPerDeciliter))
  209. }
  210. }