| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- //
- // SuspendThresholdEditor.swift
- // LoopKitUI
- //
- // Created by Michael Pangburn on 4/10/20.
- // Copyright © 2020 LoopKit Authors. All rights reserved.
- //
- import SwiftUI
- import HealthKit
- import LoopKit
- public struct SuspendThresholdEditor: View {
- var initialValue: HKQuantity?
- var unit: HKUnit
- var maxValue: HKQuantity?
- var save: (_ suspendThreshold: HKQuantity) -> Void
- let mode: SettingsPresentationMode
-
- @State private var userDidTap: Bool = false
- @State var value: HKQuantity
- @State var isEditing = false
- @State var showingConfirmationAlert = false
- @Environment(\.dismiss) var dismiss
- @Environment(\.authenticate) var authenticate
- @Environment(\.appName) private var appName
- let guardrail = Guardrail.suspendThreshold
- public init(
- value: HKQuantity?,
- unit: HKUnit,
- maxValue: HKQuantity?,
- onSave save: @escaping (_ suspendThreshold: HKQuantity) -> Void,
- mode: SettingsPresentationMode = .settings
- ) {
- self._value = State(initialValue: value ?? Self.defaultValue(for: unit))
- self.initialValue = value
- self.unit = unit
- self.maxValue = maxValue
- self.save = save
- self.mode = mode
- }
-
- public init(
- viewModel: TherapySettingsViewModel,
- didSave: (() -> Void)? = nil
- ) {
- let unit = viewModel.therapySettings.glucoseUnit ?? viewModel.preferredGlucoseUnit
- self.init(
- value: viewModel.therapySettings.suspendThreshold?.quantity,
- unit: unit,
- maxValue: Guardrail.maxSuspendThresholdValue(
- correctionRangeSchedule: viewModel.therapySettings.glucoseTargetRangeSchedule,
- preMealTargetRange: viewModel.therapySettings.preMealTargetRange?.quantityRange(for: unit),
- workoutTargetRange: viewModel.therapySettings.workoutTargetRange?.quantityRange(for: unit)
- ),
- onSave: { [weak viewModel] newValue in
- guard let viewModel = viewModel else {
- return
- }
- let newThreshold = GlucoseThreshold(unit: viewModel.preferredGlucoseUnit, value: newValue.doubleValue(for: viewModel.preferredGlucoseUnit))
- viewModel.saveSuspendThreshold(value: newThreshold)
- didSave?()
- },
- mode: viewModel.mode
- )
- }
- private static func defaultValue(for unit: HKUnit) -> HKQuantity {
- switch unit {
- case .milligramsPerDeciliter:
- return HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)
- case .millimolesPerLiter:
- return HKQuantity(unit: .millimolesPerLiter, doubleValue: 4.5)
- default:
- fatalError("Unsupported glucose unit \(unit)")
- }
- }
- public var body: some View {
- switch mode {
- case .settings: return AnyView(contentWithCancel)
- case .acceptanceFlow: return AnyView(content)
- }
- }
-
- private var contentWithCancel: some View {
- if value == initialValue {
- return AnyView(content
- .navigationBarBackButtonHidden(false)
- .navigationBarItems(leading: EmptyView())
- )
- } else {
- return AnyView(content
- .navigationBarBackButtonHidden(true)
- .navigationBarItems(leading: cancelButton)
- )
- }
- }
-
- private var cancelButton: some View {
- Button(action: { self.dismiss() } ) { Text(LocalizedString("Cancel", comment: "Cancel editing settings button title")) }
- }
-
- private var content: some View {
- ConfigurationPage(
- title: Text(TherapySetting.suspendThreshold.title),
- actionButtonTitle: Text(mode.buttonText),
- actionButtonState: saveButtonState,
- cards: {
- // TODO: Remove conditional when Swift 5.3 ships
- // https://bugs.swift.org/browse/SR-11628
- if true {
- Card {
- SettingDescription(text: description, informationalContent: { TherapySetting.suspendThreshold.helpScreen() })
- ExpandableSetting(
- isEditing: $isEditing,
- valueContent: {
- GuardrailConstrainedQuantityView(
- value: value,
- unit: unit,
- guardrail: guardrail,
- isEditing: isEditing,
- // Workaround for strange animation behavior on appearance
- forceDisableAnimations: true
- )
- },
- expandedContent: {
- GlucoseValuePicker(
- value: self.$value.animation(),
- unit: self.unit,
- guardrail: self.guardrail,
- bounds: self.guardrail.absoluteBounds.lowerBound...(self.maxValue ?? self.guardrail.absoluteBounds.upperBound)
- )
- // Prevent the picker from expanding the card's width on small devices
- .frame(maxWidth: UIScreen.main.bounds.width - 48)
- .clipped()
- }
- )
- }
- }
- },
- actionAreaContent: {
- instructionalContentIfNecessary
- if warningThreshold != nil && (userDidTap || mode != .acceptanceFlow) {
- SuspendThresholdGuardrailWarning(safetyClassificationThreshold: warningThreshold!)
- }
- },
- action: {
- if self.warningThreshold == nil {
- self.startSaving()
- } else {
- self.showingConfirmationAlert = true
- }
- }
- )
- .alert(isPresented: $showingConfirmationAlert, content: confirmationAlert)
- .navigationBarTitle("", displayMode: .inline)
- .onTapGesture {
- self.userDidTap = true
- }
- }
- var description: Text {
- Text(TherapySetting.suspendThreshold.descriptiveText(appName: appName))
- }
-
- private var instructionalContentIfNecessary: some View {
- return Group {
- if mode == .acceptanceFlow && !userDidTap {
- instructionalContent
- }
- }
- }
- private var instructionalContent: some View {
- HStack { // to align with guardrail warning, if present
- Text(LocalizedString("You can edit the setting by tapping into the line item.", comment: "Description of how to edit setting"))
- .foregroundColor(.secondary)
- .font(.subheadline)
- Spacer()
- }
- }
- private var saveButtonState: ConfigurationPageActionButtonState {
- initialValue == nil || value != initialValue! || mode == .acceptanceFlow ? .enabled : .disabled
- }
- private var warningThreshold: SafetyClassification.Threshold? {
- switch guardrail.classification(for: value) {
- case .withinRecommendedRange:
- return nil
- case .outsideRecommendedRange(let threshold):
- return threshold
- }
- }
- private func confirmationAlert() -> SwiftUI.Alert {
- SwiftUI.Alert(
- title: Text(LocalizedString("Save Glucose Safety Limit?", comment: "Alert title for confirming a glucose safety limit outside the recommended range")),
- message: Text(TherapySetting.suspendThreshold.guardrailSaveWarningCaption),
- primaryButton: .cancel(Text(LocalizedString("Go Back", comment: "Text for go back action on confirmation alert"))),
- secondaryButton: .default(
- Text(LocalizedString("Continue", comment: "Text for continue action on confirmation alert")),
- action: startSaving
- )
- )
- }
-
- private func startSaving() {
- guard mode == .settings else {
- self.continueSaving()
- return
- }
- authenticate(TherapySetting.suspendThreshold.authenticationChallengeDescription) {
- switch $0 {
- case .success: self.continueSaving()
- case .failure: break
- }
- }
- }
-
- private func continueSaving() {
- self.save(self.value)
- }
- }
- struct SuspendThresholdGuardrailWarning: View {
- var safetyClassificationThreshold: SafetyClassification.Threshold
- var body: some View {
- GuardrailWarning(title: title, threshold: safetyClassificationThreshold)
- }
- private var title: Text {
- switch safetyClassificationThreshold {
- case .minimum, .belowRecommended:
- return Text(LocalizedString("Low Glucose Safety Limit", comment: "Title text for the low glucose safety limit warning"))
- case .aboveRecommended, .maximum:
- return Text(LocalizedString("High Glucose Safety Limit", comment: "Title text for the high glucose safety limit warning"))
- }
- }
- }
- struct SuspendThresholdView_Previews: PreviewProvider {
- static var previews: some View {
- SuspendThresholdEditor(value: nil, unit: .milligramsPerDeciliter, maxValue: nil, onSave: { _ in })
- }
- }
|