瀏覽代碼

Add setting for Confirm Bolus

Defines whether a confirmation dialog is triggered when attempting to bolus from the Treatment View
Mike Plante 1 年之前
父節點
當前提交
9daef8017a

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -355,6 +355,7 @@
 		BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A852D25F97D0016C40C /* TrioWatchApp.swift */; };
 		BDFF7A8B2D25F97D0016C40C /* Unit Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A8A2D25F97D0016C40C /* Unit Tests.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
+		C23B70E92D7159EB00A50DC7 /* ConfirmBolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23B70E82D7159EB00A50DC7 /* ConfirmBolus.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
@@ -1042,6 +1043,7 @@
 		BDFF7A922D25F97D0016C40C /* TrioWatchAppExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioWatchAppExtension.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsProvider.swift; sourceTree = "<group>"; };
+		C23B70E82D7159EB00A50DC7 /* ConfirmBolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmBolus.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
@@ -2072,6 +2074,7 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
+				C23B70E82D7159EB00A50DC7 /* ConfirmBolus.swift */,
 				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
 				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
 				DDD6D4D22CDE90720029439A /* HbA1cDisplayUnit.swift */,
@@ -3957,6 +3960,7 @@
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
+				C23B70E92D7159EB00A50DC7 /* ConfirmBolus.swift in Sources */,
 				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,

+ 30 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -7541,6 +7541,9 @@
         }
       }
     },
+    "•" : {
+
+    },
     "• Basal Rate" : {
       "localizations" : {
         "ar" : {
@@ -28198,6 +28201,9 @@
     "Alter the rate of dynamic sensitivity adjustments for Sigmoid." : {
 
     },
+    "Always" : {
+
+    },
     "Always Color Glucose Value (green, yellow etc)" : {
       "comment" : "UI/UX option",
       "extractionState" : "manual",
@@ -45797,6 +45803,9 @@
         }
       }
     },
+    "Confirm Bolus" : {
+
+    },
     "Confirm Bolus Faster" : {
       "localizations" : {
         "ar" : {
@@ -54559,6 +54568,9 @@
         }
       }
     },
+    "Default: Very Low Glucose" : {
+
+    },
     "Defaults to 0.2 (20%). Maximum positive percentual change of BG level to use SMB, above that will disable SMB. Hardcoded cap of 40%. For UAM fully-closed-loop 30% is advisable. Observe in log and popup (maxDelta 27 > 20% of BG 100 - disabling SMB!)." : {
       "comment" : "Max Delta-BG Threshold SMB",
       "extractionState" : "manual",
@@ -110158,6 +110170,9 @@
         }
       }
     },
+    "Never" : {
+
+    },
     "New Basal Profile =\n(Current Basal Profile) × (Autosens Ratio)" : {
       "localizations" : {
         "ar" : {
@@ -114745,6 +114760,9 @@
         }
       }
     },
+    "Note: Does not affect logging external insulin." : {
+
+    },
     "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app." : {
       "localizations" : {
         "ar" : {
@@ -161248,6 +161266,9 @@
         }
       }
     },
+    "This setting triggers a confirmation dialog before enacting a bolus as an extra safeguard, but it does not affect authentication for bolusing (Face ID, Touch ID, password)." : {
+
+    },
     "This Temp Target preset is currently running. Deleting will stop it." : {
       "localizations" : {
         "ar" : {
@@ -166743,6 +166764,9 @@
         }
       }
     },
+    "Triggers a confirmation dialog before enacting a bolus." : {
+
+    },
     "Trio Configuration" : {
       "localizations" : {
         "ar" : {
@@ -173792,6 +173816,12 @@
         }
       }
     },
+    "Very Low Forecast" : {
+
+    },
+    "Very Low Glucose" : {
+
+    },
     "Wait please" : {
       "extractionState" : "manual",
       "localizations" : {

+ 24 - 0
Trio/Sources/Models/ConfirmBolus.swift

@@ -0,0 +1,24 @@
+import Foundation
+
+enum ConfirmBolus: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case never
+    case veryLowGlucose
+    case veryLowForecast
+    case always
+    var displayName: String {
+        switch self {
+        case .never:
+            return String(localized: "Never", comment: "")
+
+        case .veryLowGlucose:
+            return String(localized: "Very Low Glucose", comment: "")
+
+        case .veryLowForecast:
+            return String(localized: "Very Low Forecast", comment: "")
+
+        case .always:
+            return String(localized: "Always", comment: "")
+        }
+    }
+}

+ 5 - 0
Trio/Sources/Models/TrioSettings.swift

@@ -69,6 +69,7 @@ struct TrioSettings: JSON, Equatable {
     var sweetMeals: Bool = false
     var sweetMealFactor: Decimal = 1
     var displayPresets: Bool = true
+    var confirmBolus: ConfirmBolus = .veryLowGlucose
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
@@ -298,6 +299,10 @@ extension TrioSettings: Decodable {
             settings.displayPresets = displayPresets
         }
 
+        if let confirmBolus = try? container.decode(ConfirmBolus.self, forKey: .confirmBolus) {
+            settings.confirmBolus = confirmBolus
+        }
+
         if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
             settings.useLiveActivity = useLiveActivity
         }

+ 2 - 0
Trio/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -9,6 +9,7 @@ extension BolusCalculatorConfig {
         @Published var sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var displayPresets: Bool = true
+        @Published var confirmBolus: ConfirmBolus = .veryLowGlucose
 
         override func subscribe() {
             units = settingsManager.settings.units
@@ -19,6 +20,7 @@ extension BolusCalculatorConfig {
             subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor) { fattyMealFactor = $0 }
             subscribeSetting(\.sweetMeals, on: $sweetMeals) { sweetMeals = $0 }
             subscribeSetting(\.sweetMealFactor, on: $sweetMealFactor) { sweetMealFactor = $0 }
+            subscribeSetting(\.confirmBolus, on: $confirmBolus) { confirmBolus = $0 }
         }
     }
 }

+ 74 - 0
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -152,6 +152,80 @@ extension BolusCalculatorConfig {
                         Text("This could be useful for fast absorbing meals like sugary cereal.")
                     }
                 )
+
+                Section {
+                    VStack {
+                        Picker(
+                            selection: $state.confirmBolus,
+                            label: Text("Confirm Bolus")
+                        ) {
+                            ForEach(ConfirmBolus.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Triggers a confirmation dialog before enacting a bolus."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = String(localized: "Confirm Bolus")
+                                    selectedVerboseHint =
+                                        AnyView(
+                                            VStack(alignment: .leading, spacing: 10) {
+                                                Text("Default: Very Low Glucose")
+                                                    .bold()
+
+                                                Text(
+                                                    "This setting triggers a confirmation dialog before enacting a bolus as an extra safeguard, but it does not affect authentication for bolusing (Face ID, Touch ID, password)."
+                                                )
+
+                                                VStack(alignment: .leading) {
+                                                    let bulletOptions: [(title: String, description: String)] = [
+                                                        ("Never", "Disables the bolus confirmation dialog."),
+                                                        (
+                                                            "Very Low Glucose",
+                                                            "Only confirm bolus when current glucose is < \(state.units == .mgdL ? "54" : 54.formattedAsMmolL) \(state.units.rawValue)."
+                                                        ),
+                                                        (
+                                                            "Very Low Forecast",
+                                                            "Confirm bolus when either current glucose is < \(state.units == .mgdL ? "54" : 54.formattedAsMmolL) \(state.units.rawValue), or glucose is forecasted < \(state.units == .mgdL ? "54" : 54.formattedAsMmolL) \(state.units.rawValue) by minPredBG."
+                                                        ),
+                                                        (
+                                                            "Always",
+                                                            "Always confirm before enacting a bolus."
+                                                        )
+                                                    ]
+
+                                                    ForEach(bulletOptions, id: \.title) { option in
+                                                        HStack(alignment: .firstTextBaseline, spacing: 2) {
+                                                            Text("•").padding(.trailing, 2)
+
+                                                            Text(option.title).bold() +
+                                                                Text(": " + option.description)
+                                                        }
+                                                    }
+                                                }
+
+                                                Text("Note: Does not affect logging external insulin.")
+                                            }
+                                        )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {

+ 2 - 0
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -70,6 +70,7 @@ extension Treatments {
         var fattyMealFactor: Decimal = 0
         var useFattyMealCorrectionFactor: Bool = false
         var displayPresets: Bool = true
+        var confirmBolus: ConfirmBolus = .veryLowGlucose
 
         var currentBasal: Decimal = 0
         var currentCarbRatio: Decimal = 0
@@ -287,6 +288,7 @@ extension Treatments {
             sweetMeals = settings.settings.sweetMeals
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
+            confirmBolus = settings.settings.confirmBolus
             forecastDisplayType = settings.settings.forecastDisplayType
             lowGlucose = settingsManager.settings.low
             highGlucose = settingsManager.settings.high

+ 26 - 16
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -381,22 +381,29 @@ extension Treatments {
 
         @State private var showConfirmDialogForBolusing = false
 
-        private var bolusWarning: String {
+        private var bolusWarning: (shouldConfirm: Bool, warningMessage: String) {
             let isGlucoseVeryLow = state.currentBG < 54
-            let isMinPredBGVeryLow = state.minPredBG < 54
+            let isForecastVeryLow = state.minPredBG < 54
 
-            guard !state.externalInsulin,
-                  state.amount > 0,
-                  isGlucoseVeryLow || isMinPredBGVeryLow
-            else {
-                return ""
+            // Only warn when enacting a bolus via pump
+            guard !state.externalInsulin, state.amount > 0 else {
+                return (false, "")
             }
 
-            let warning = isGlucoseVeryLow
-                ? "Glucose is very low."
-                : "Glucose forecast is very low."
+            let warningMessage = switch (isGlucoseVeryLow, isForecastVeryLow) {
+            case (true, _): "Glucose is very low."
+            case (_, true): "Glucose forecast is very low."
+            default: ""
+            }
+
+            let shouldConfirm = switch state.confirmBolus {
+            case .never: false
+            case .always: true
+            case .veryLowGlucose: isGlucoseVeryLow
+            case .veryLowForecast: isGlucoseVeryLow || isForecastVeryLow
+            }
 
-            return warning
+            return (shouldConfirm, warningMessage)
         }
 
         var treatmentButton: some View {
@@ -409,7 +416,7 @@ extension Treatments {
 
             return Section {
                 Button {
-                    if bolusWarning != "" {
+                    if bolusWarning.shouldConfirm {
                         showConfirmDialogForBolusing = true
                     } else {
                         state.invokeTreatmentsTask()
@@ -433,18 +440,21 @@ extension Treatments {
                 .shadow(radius: 3)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .confirmationDialog(
-                    bolusWarning + " Bolus \(state.amount.description) U?",
+                    bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
                     isPresented: $showConfirmDialogForBolusing,
                     titleVisibility: .visible
                 ) {
                     Button("Cancel", role: .cancel) {}
-                    Button("Ignore Warning and Enact Bolus", role: .destructive) {
+                    Button(
+                        bolusWarning.warningMessage.isEmpty ? "Enact Bolus" : "Ignore Warning and Enact Bolus",
+                        role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                    ) {
                         state.invokeTreatmentsTask()
                     }
                 }
             } header: {
-                if bolusWarning != "" {
-                    Text(bolusWarning)
+                if !bolusWarning.warningMessage.isEmpty {
+                    Text(bolusWarning.warningMessage)
                         .textCase(nil)
                         .font(.subheadline)
                         .foregroundColor(Color.loopRed)