Просмотр исходного кода

Quick bolus: redesign sheet with pills, slide-to-enact, and settings toggle

- Replace List-based picker with 3 capsule pills in an HStack; tap to select
- Replace confirmation dialog + biometric with a slide-to-enact control
  (SlideToConfirmView) that triggers Face ID/Touch ID on completion
- Add QuickBolusInfoView with "Got it!" button replacing toolbar Done
- Add QuickBolusConfig settings module with enableQuickBolus toggle,
  wired into TrioSettings and HomeStateModel; scoring is skipped when disabled
- Add "Quick Bolus" entry to Feature Settings between Bolus Calculator
  and Meal Settings
- Move all quick bolus UI out of HomeRootView into focused fragment files
  (QuickBolusView, QuickBolusInfoView, SlideToConfirmView)
- Fix bolusFormatter minimumIntegerDigits 0→1 so 0.5 renders as "0.5"
- Replace "Deliver" with "Enact" throughout quick bolus UI and strings
- Use safeAreaInset for slide control so subtitle text is never cropped
  on small screens (iPhone mini)
Magnus Reintz 1 неделя назад
Родитель
Сommit
f6e7d277a3

+ 52 - 0
Trio.xcodeproj/project.pbxproj

@@ -480,6 +480,13 @@
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
+		786B6E8A8E294AE883581154 /* QuickBolusConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D543DE17E0054AF0821411BB /* QuickBolusConfigDataFlow.swift */; };
+		FA6B58486AE2496F9F8589C6 /* QuickBolusConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEC5C2927FA4C5DA61A1A40 /* QuickBolusConfigProvider.swift */; };
+		87F4D02B8D4D4D329BA291D2 /* QuickBolusConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91711E7CA54747EFAD6540EE /* QuickBolusConfigStateModel.swift */; };
+		C5BAAA36C643401D97A8CE11 /* QuickBolusConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EBC8BA17C4627BD32C58F /* QuickBolusConfigRootView.swift */; };
+		584C79A91D034426A1DE60CB /* SlideToConfirmView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF96FC200503405E94334390 /* SlideToConfirmView.swift */; };
+		DECF1903237041E2BBBE4033 /* QuickBolusInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1526A587AD1544C495050662 /* QuickBolusInfoView.swift */; };
+		3F23E18680094E6DA98628E4 /* QuickBolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A54068ABDAE4898B243DF14 /* QuickBolusView.swift */; };
 		BD7DB88E2D2C4A17003D3155 /* BolusCalculationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */; };
 		BD8207C42D2B42E60023339D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		BD8207C52D2B42E60023339D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
@@ -1422,6 +1429,13 @@
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
+		D543DE17E0054AF0821411BB /* QuickBolusConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickBolusConfigDataFlow.swift; sourceTree = "<group>"; };
+		2EEC5C2927FA4C5DA61A1A40 /* QuickBolusConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickBolusConfigProvider.swift; sourceTree = "<group>"; };
+		91711E7CA54747EFAD6540EE /* QuickBolusConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickBolusConfigStateModel.swift; sourceTree = "<group>"; };
+		DB3EBC8BA17C4627BD32C58F /* QuickBolusConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickBolusConfigRootView.swift; sourceTree = "<group>"; };
+		CF96FC200503405E94334390 /* SlideToConfirmView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideToConfirmView.swift; sourceTree = "<group>"; };
+		1526A587AD1544C495050662 /* QuickBolusInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickBolusInfoView.swift; sourceTree = "<group>"; };
+		8A54068ABDAE4898B243DF14 /* QuickBolusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickBolusView.swift; sourceTree = "<group>"; };
 		BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculationManager.swift; sourceTree = "<group>"; };
 		BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Trio Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
 		BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProvider.swift; sourceTree = "<group>"; };
@@ -2156,6 +2170,7 @@
 				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
 				3811DE3825C9D4A100A708ED /* Settings */,
 				110AEDEA2C51A0AE00615CC9 /* ShortcutsConfig */,
+				08A79EA89BFA498F80F4D6FE /* QuickBolusConfig */,
 				DD17451E2C55520000211FAC /* SMBSettings */,
 				29B478DF61BF8D270F7D8954 /* Snooze */,
 				19F95FF129F10F9C00314DDC /* Stat */,
@@ -2248,10 +2263,21 @@
 				3811DE2E25C9D49500A708ED /* HomeRootView.swift */,
 				3833B51E260264AC003021B3 /* Chart */,
 				3833B51F260264B6003021B3 /* Header */,
+				FDB1D1005DA54C048E622761 /* QuickBolus */,
 			);
 			path = View;
 			sourceTree = "<group>";
 		};
+		FDB1D1005DA54C048E622761 /* QuickBolus */ = {
+			isa = PBXGroup;
+			children = (
+				CF96FC200503405E94334390 /* SlideToConfirmView.swift */,
+				1526A587AD1544C495050662 /* QuickBolusInfoView.swift */,
+				8A54068ABDAE4898B243DF14 /* QuickBolusView.swift */,
+			);
+			path = QuickBolus;
+			sourceTree = "<group>";
+		};
 		3811DE3825C9D4A100A708ED /* Settings */ = {
 			isa = PBXGroup;
 			children = (
@@ -3381,6 +3407,25 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		08A79EA89BFA498F80F4D6FE /* QuickBolusConfig */ = {
+			isa = PBXGroup;
+			children = (
+				D543DE17E0054AF0821411BB /* QuickBolusConfigDataFlow.swift */,
+				2EEC5C2927FA4C5DA61A1A40 /* QuickBolusConfigProvider.swift */,
+				91711E7CA54747EFAD6540EE /* QuickBolusConfigStateModel.swift */,
+				2148032F6F184487B49B1095 /* View */,
+			);
+			path = QuickBolusConfig;
+			sourceTree = "<group>";
+		};
+		2148032F6F184487B49B1095 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DB3EBC8BA17C4627BD32C58F /* QuickBolusConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		BD7DB88C2D2C49FF003D3155 /* BolusCalculator */ = {
 			isa = PBXGroup;
 			children = (
@@ -4897,6 +4942,13 @@
 				3B2A3BC12E2B19C600658FB9 /* DynamicISF.swift in Sources */,
 				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
+				584C79A91D034426A1DE60CB /* SlideToConfirmView.swift in Sources */,
+				DECF1903237041E2BBBE4033 /* QuickBolusInfoView.swift in Sources */,
+				3F23E18680094E6DA98628E4 /* QuickBolusView.swift in Sources */,
+				786B6E8A8E294AE883581154 /* QuickBolusConfigDataFlow.swift in Sources */,
+				FA6B58486AE2496F9F8589C6 /* QuickBolusConfigProvider.swift in Sources */,
+				87F4D02B8D4D4D329BA291D2 /* QuickBolusConfigStateModel.swift in Sources */,
+				C5BAAA36C643401D97A8CE11 /* QuickBolusConfigRootView.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				3E62C7822F54CC1B00433237 /* BolusDisplayThreshold.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,

+ 27 - 20
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -27218,7 +27218,7 @@
       }
     },
     "About Quick Bolus" : {
-      "comment" : "A button that displays a sheet with information about Quick Bolus."
+      "comment" : "Info sheet title for quick bolus feature"
     },
     "About the Process" : {
       "extractionState" : "manual",
@@ -55327,9 +55327,6 @@
         }
       }
     },
-    "Based on your bolus history at this time of day" : {
-      "comment" : "A description of the quick bolus confirmation dialog."
-    },
     "Basic Insulin Rates & Targets" : {
       "localizations" : {
         "bg" : {
@@ -81704,7 +81701,7 @@
       }
     },
     "d" : {
-      "comment" : "abbreviation for days",
+      "comment" : "Abbreviation for Days\nabbreviation for days",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -93012,12 +93009,6 @@
         }
       }
     },
-    "Deliver" : {
-
-    },
-    "Deliver %@ U?" : {
-      "comment" : "A button title that asks the user to confirm the amount of insulin to be delivered. The argument is the amount of insulin."
-    },
     "Delivery limits" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -106543,6 +106534,9 @@
         }
       }
     },
+    "Enable Quick Bolus" : {
+      "comment" : "Toggle label for the quick bolus feature setting"
+    },
     "Enable Reduced Bolus Option" : {
       "localizations" : {
         "bg" : {
@@ -131780,6 +131774,7 @@
       }
     },
     "Got it!" : {
+      "comment" : "Dismiss button for Quick Bolus info sheet",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -132788,7 +132783,7 @@
       }
     },
     "h" : {
-      "comment" : "abbreviation for hours\nh",
+      "comment" : "Abbreviation for Hours\nabbreviation for hours\nh",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -137743,9 +137738,6 @@
         }
       }
     },
-    "How does this work?" : {
-      "comment" : "A button that shows a message explaining how the Quick Bolus feature works."
-    },
     "How it Works" : {
       "localizations" : {
         "bg" : {
@@ -158248,6 +158240,9 @@
         }
       }
     },
+    "Long-press the + button on the home screen to enact a quick bolus." : {
+      "comment" : "Mini hint for the enable quick bolus toggle"
+    },
     "Loop at %@ failed." : {
       "localizations" : {
         "bg" : {
@@ -180312,7 +180307,7 @@
 
     },
     "No Quick Bolus History Yet" : {
-      "comment" : "A message that appears when the user has not yet selected a quick bolus amount."
+      "comment" : "Alert title when no quick bolus history exists"
     },
     "No recent oref algorithm determination." : {
       "localizations" : {
@@ -204803,13 +204798,13 @@
       }
     },
     "Quick Bolus" : {
-      "comment" : "A button that presents a quick bolus selection sheet."
+      "comment" : "Title of the quick bolus sheet"
     },
     "Quick Bolus learns from your manual boluses over time. Once you've delivered a few boluses, it will suggest amounts based on what you typically take at this time of day." : {
-      "comment" : "A description of how Quick Bolus works."
+      "comment" : "Alert body explaining that quick bolus history is empty"
     },
-    "Quick Bolus looks at your manual boluses from the past 90 days and suggests the amounts you most commonly take at this time of day.\n\nIt gives more weight to boluses from similar times of day, and treats weekdays and weekends separately. Older entries gradually count less.\n\nTap a suggestion to confirm and deliver it — your normal Face ID or Touch ID approval always applies." : {
-      "comment" : "A description of Quick Bolus."
+    "Quick Bolus looks at your manual boluses from the past 90 days and suggests the amounts you most commonly take at this time of day.\n\nIt gives more weight to boluses from similar times of day, and treats weekdays and weekends separately. Older entries gradually count less.\n\nTap a suggestion to select it, then slide to confirm. Your normal Face ID or Touch ID approval always applies." : {
+      "comment" : "Info sheet body explaining how quick bolus scoring works"
     },
     "Raise target glucose when Autosens Ratio is less than 1." : {
       "localizations" : {
@@ -227506,6 +227501,12 @@
         }
       }
     },
+    "Slide to confirm your selected amount. Face ID or Touch ID is always required before the bolus is enacted." : {
+      "comment" : "Verbose hint for enable quick bolus toggle, part 2"
+    },
+    "Slide to Enact Bolus" : {
+      "comment" : "Slide to confirm label for quick bolus"
+    },
     "Slope" : {
       "comment" : "v",
       "localizations" : {
@@ -284249,6 +284250,9 @@
         }
       }
     },
+    "When enabled, long-pressing the + button on the home screen opens a Quick Bolus sheet. It suggests up to three bolus amounts based on your bolus history at similar times of day, weighted by recency and day type (weekday vs. weekend)." : {
+      "comment" : "Verbose hint for enable quick bolus toggle, part 1"
+    },
     "When enabled, Super Micro Boluses (SMBs) will always be allowed if dosing calculations determine insulin is needed via the SMB delivery method, except when a high Temp Target is set." : {
       "localizations" : {
         "bg" : {
@@ -291021,6 +291025,9 @@
         }
       }
     },
+    "Your most-used bolus amounts at similar times on similar days. Tap one to pick it." : {
+      "comment" : "Subtitle of the quick bolus pill row"
+    },
     "Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors" : {
       "extractionState" : "manual",
       "localizations" : {

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

@@ -70,6 +70,7 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var sweetMealFactor: Decimal = 1
     var displayPresets: Bool = true
     var confirmBolus: Bool = false
+    var enableQuickBolus: Bool = false
     var useLiveActivity: Bool = false
     var lockScreenView: LockScreenView = .simple
     var smartStackView: LockScreenView = .simple

+ 4 - 0
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -121,6 +121,7 @@ extension Home {
         var shouldRunDeleteOnSettingsChange = true
 
         var showCarbsRequiredBadge: Bool = true
+        var enableQuickBolus: Bool = false
         var quickBolusHistory: [Decimal] = []
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         var minForecast: [Int] = []
@@ -480,6 +481,7 @@ extension Home {
             bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
             thresholdLines = settingsManager.settings.rulerMarks
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
+            enableQuickBolus = settingsManager.settings.enableQuickBolus
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
@@ -537,6 +539,7 @@ extension Home {
         }
 
         func loadQuickBolusSuggestions() async {
+            guard enableQuickBolus else { return }
             let cutoff = Calendar.current.date(byAdding: .day, value: -90, to: Date()) ?? Date()
             let predicate = NSPredicate(
                 format: "isSMB == false AND isExternal == false AND pumpEvent.timestamp >= %@",
@@ -894,6 +897,7 @@ extension Home.StateModel:
         thresholdLines = settingsManager.settings.rulerMarks
         bolusDisplayThreshold = settingsManager.settings.bolusDisplayThreshold
         showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
+        enableQuickBolus = settingsManager.settings.enableQuickBolus
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         displayPumpStatusHighlightMessage()

+ 6 - 89
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -33,10 +33,7 @@ extension Home {
         @State var showTreatments = false
         @State var selectedTab: Int = 0
         @State var showQuickBolusPicker = false
-        @State var quickBolusAmount: Decimal = 0
-        @State var showQuickBolusConfirm = false
         @State var showQuickBolusNoHistory = false
-        @State var showQuickBolusInfo = false
         @State var showPumpSelection: Bool = false
         @State var showCGMSelection: Bool = false
         @State var notificationsDisabled = false
@@ -1124,6 +1121,7 @@ extension Home {
                         state.showModal(for: .treatmentView)
                     }
                     .onLongPressGesture(minimumDuration: 0.5) {
+                        guard state.enableQuickBolus else { return }
                         Task {
                             await state.loadQuickBolusSuggestions()
                             if state.quickBolusHistory.isEmpty {
@@ -1141,22 +1139,6 @@ extension Home {
                 }
         }
 
-        private func formatBolusLabel(_ amount: Decimal) -> String {
-            let formatted = Formatter.bolusFormatter.string(from: amount as NSDecimalNumber) ?? amount.description
-            return "\(formatted) U"
-        }
-
-        private func formatBolusConfirmTitle(_ amount: Decimal) -> String {
-            let formatted = Formatter.bolusFormatter.string(from: amount as NSDecimalNumber) ?? amount.description
-            return String(
-                format: String(
-                    localized: "Deliver %@ U?",
-                    comment: "Quick bolus confirmation dialog title; %@ is the formatted bolus amount"
-                ),
-                formatted
-            )
-        }
-
         var body: some View {
             ZStack(alignment: .center) {
                 tabBar()
@@ -1166,76 +1148,11 @@ extension Home {
                 }
             }
             .sheet(isPresented: $showQuickBolusPicker) {
-                NavigationStack {
-                    List {
-                        Section {
-                            ForEach(state.quickBolusHistory, id: \.self) { amount in
-                                Button(formatBolusLabel(amount)) {
-                                    quickBolusAmount = amount
-                                    showQuickBolusPicker = false
-                                    showQuickBolusConfirm = true
-                                }
-                                .foregroundStyle(.primary)
-                            }
-                        } footer: {
-                            Text(String(
-                                localized: "Based on your bolus history at this time of day",
-                                comment: "Subtitle of the quick bolus picker sheet"
-                            ))
-                        }
-                    }
-                    .navigationTitle(String(localized: "Quick Bolus", comment: "Title of the quick bolus picker sheet"))
-                    .navigationBarTitleDisplayMode(.inline)
-                    .toolbar {
-                        ToolbarItem(placement: .cancellationAction) {
-                            Button(String(localized: "Cancel")) {
-                                showQuickBolusPicker = false
-                            }
-                        }
-                        ToolbarItem(placement: .topBarTrailing) {
-                            Button {
-                                showQuickBolusInfo = true
-                            } label: {
-                                Image(systemName: "questionmark.circle")
-                            }
-                        }
-                    }
-                    .sheet(isPresented: $showQuickBolusInfo) {
-                        NavigationStack {
-                            ScrollView {
-                                Text(String(
-                                    localized: "Quick Bolus looks at your manual boluses from the past 90 days and suggests the amounts you most commonly take at this time of day.\n\nIt gives more weight to boluses from similar times of day, and treats weekdays and weekends separately. Older entries gradually count less.\n\nTap a suggestion to confirm and deliver it — your normal Face ID or Touch ID approval always applies.",
-                                    comment: "Info sheet body explaining how quick bolus scoring works"
-                                ))
-                                    .padding()
-                            }
-                            .navigationTitle(String(
-                                localized: "About Quick Bolus",
-                                comment: "Info sheet title for quick bolus feature"
-                            ))
-                            .navigationBarTitleDisplayMode(.inline)
-                            .toolbar {
-                                ToolbarItem(placement: .confirmationAction) {
-                                    Button(String(localized: "Done")) {
-                                        showQuickBolusInfo = false
-                                    }
-                                }
-                            }
-                        }
-                        .presentationDetents([.medium])
-                    }
-                }
-                .presentationDetents([.height(CGFloat(state.quickBolusHistory.count) * 52 + 140)])
-            }
-            .confirmationDialog(
-                formatBolusConfirmTitle(quickBolusAmount),
-                isPresented: $showQuickBolusConfirm,
-                titleVisibility: .visible
-            ) {
-                Button(String(localized: "Deliver", comment: "Quick bolus confirm button"), role: .destructive) {
-                    Task { await state.enactQuickBolus(amount: quickBolusAmount) }
-                }
-                Button(String(localized: "Cancel"), role: .cancel) {}
+                QuickBolusView(
+                    suggestions: state.quickBolusHistory,
+                    onEnact: { amount in await state.enactQuickBolus(amount: amount) },
+                    isPresented: $showQuickBolusPicker
+                )
             }
             .alert(
                 String(localized: "No Quick Bolus History Yet", comment: "Alert title when no quick bolus history exists"),

+ 36 - 0
Trio/Sources/Modules/Home/View/QuickBolus/QuickBolusInfoView.swift

@@ -0,0 +1,36 @@
+import SwiftUI
+
+struct QuickBolusInfoView: View {
+    @Binding var isPresented: Bool
+
+    @State private var detent = PresentationDetent.medium
+
+    var body: some View {
+        NavigationStack {
+            ScrollView {
+                Text(String(
+                    localized: "Quick Bolus looks at your manual boluses from the past 90 days and suggests the amounts you most commonly take at this time of day.\n\nIt gives more weight to boluses from similar times of day, and treats weekdays and weekends separately. Older entries gradually count less.\n\nTap a suggestion to select it, then slide to confirm. Your normal Face ID or Touch ID approval always applies.",
+                    comment: "Info sheet body explaining how quick bolus scoring works"
+                ))
+                    .padding()
+            }
+            .navigationTitle(String(
+                localized: "About Quick Bolus",
+                comment: "Info sheet title for quick bolus feature"
+            ))
+            .navigationBarTitleDisplayMode(.inline)
+
+            Button {
+                isPresented = false
+            } label: {
+                Text("Got it!", comment: "Dismiss button for Quick Bolus info sheet")
+                    .bold()
+                    .frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
+            }
+            .buttonStyle(.bordered)
+            .padding([.horizontal, .bottom])
+            .padding(.top, 4)
+        }
+        .presentationDetents([.medium], selection: $detent)
+    }
+}

+ 98 - 0
Trio/Sources/Modules/Home/View/QuickBolus/QuickBolusView.swift

@@ -0,0 +1,98 @@
+import SwiftUI
+
+struct QuickBolusView: View {
+    let suggestions: [Decimal]
+    let onEnact: (Decimal) async -> Void
+    @Binding var isPresented: Bool
+
+    @State private var selectedAmount: Decimal?
+    @State private var showInfo = false
+
+    var body: some View {
+        NavigationStack {
+            VStack(spacing: 20) {
+                pillRow
+                    .padding(.top, 8)
+
+                Text(
+                    "Your most-used bolus amounts at similar times on similar days. Tap one to pick it.",
+                    comment: "Subtitle of the quick bolus pill row"
+                )
+                .font(.subheadline)
+                .foregroundStyle(.secondary)
+                .multilineTextAlignment(.center)
+                .padding(.horizontal)
+            }
+            .frame(maxWidth: .infinity)
+            .safeAreaInset(edge: .bottom, spacing: 0) {
+                SlideToConfirmView(
+                    label: String(localized: "Slide to Enact Bolus", comment: "Slide to confirm label for quick bolus"),
+                    isEnabled: selectedAmount != nil
+                ) {
+                    guard let amount = selectedAmount else { return }
+                    isPresented = false
+                    Task { await onEnact(amount) }
+                }
+                .padding(.horizontal)
+                .padding(.vertical, 12)
+            }
+            .navigationTitle(String(localized: "Quick Bolus", comment: "Title of the quick bolus sheet"))
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button {
+                        showInfo = true
+                    } label: {
+                        Image(systemName: "info.circle")
+                    }
+                }
+            }
+            .sheet(isPresented: $showInfo) {
+                QuickBolusInfoView(isPresented: $showInfo)
+            }
+        }
+        .presentationDetents([.height(260)])
+    }
+
+    private var displayedSuggestions: [Decimal] {
+        Array(suggestions.sorted().prefix(3))
+    }
+
+    private var pillRow: some View {
+        HStack(spacing: 12) {
+            ForEach(displayedSuggestions, id: \.self) { amount in
+                bolusAmountPill(amount)
+            }
+        }
+        .padding(.horizontal)
+    }
+
+    private func bolusAmountPill(_ amount: Decimal) -> some View {
+        let isSelected = selectedAmount == amount
+        let formatted = Formatter.bolusFormatter.string(from: amount as NSDecimalNumber) ?? amount.description
+
+        return Button {
+            selectedAmount = amount
+        } label: {
+            HStack(alignment: .firstTextBaseline, spacing: 2) {
+                Text(formatted)
+                    .font(.title2.bold())
+                Text("U")
+                    .font(.callout)
+                    .foregroundStyle(isSelected ? .white.opacity(0.85) : .secondary)
+            }
+            .frame(maxWidth: .infinity, minHeight: 64)
+            .background(isSelected ? Color.accentColor : Color(.secondarySystemFill))
+            .foregroundStyle(isSelected ? .white : .primary)
+            .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
+            .overlay {
+                if isSelected {
+                    RoundedRectangle(cornerRadius: 18, style: .continuous)
+                        .strokeBorder(Color.accentColor, lineWidth: 2.5)
+                }
+            }
+        }
+        .buttonStyle(.plain)
+        .animation(.easeInOut(duration: 0.15), value: isSelected)
+    }
+}

+ 58 - 0
Trio/Sources/Modules/Home/View/QuickBolus/SlideToConfirmView.swift

@@ -0,0 +1,58 @@
+import SwiftUI
+
+struct SlideToConfirmView: View {
+    let label: String
+    let isEnabled: Bool
+    let action: () -> Void
+
+    @State private var dragOffset: CGFloat = 0
+
+    private let thumbSize: CGFloat = 52
+    private let trackHeight: CGFloat = 56
+    private let completionThreshold: CGFloat = 0.85
+
+    var body: some View {
+        GeometryReader { geo in
+            let maxDrag = geo.size.width - thumbSize - 8
+            let progress = maxDrag > 0 ? dragOffset / maxDrag : 0
+
+            ZStack(alignment: .leading) {
+                Capsule()
+                    .fill(isEnabled ? Color.accentColor.opacity(0.25) : Color.secondary.opacity(0.2))
+
+                Text(label)
+                    .font(.headline)
+                    .foregroundStyle(isEnabled ? .white.opacity(1 - progress * 1.5) : .secondary)
+                    .frame(maxWidth: .infinity)
+
+                RoundedRectangle(cornerRadius: thumbSize / 2)
+                    .fill(isEnabled ? Color.accentColor : Color.secondary.opacity(0.4))
+                    .frame(width: thumbSize, height: thumbSize)
+                    .overlay {
+                        Image(systemName: "chevron.right.2")
+                            .foregroundStyle(.white)
+                            .font(.system(size: 18, weight: .semibold))
+                    }
+                    .offset(x: 4 + dragOffset)
+                    .gesture(
+                        isEnabled ? DragGesture()
+                            .onChanged { value in
+                                dragOffset = min(max(0, value.translation.width), maxDrag)
+                            }
+                            .onEnded { _ in
+                                if dragOffset >= maxDrag * completionThreshold {
+                                    UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
+                                    action()
+                                    withAnimation(.spring(duration: 0.4)) { dragOffset = 0 }
+                                } else {
+                                    withAnimation(.spring()) { dragOffset = 0 }
+                                }
+                            } : nil
+                    )
+                    .animation(.interactiveSpring(), value: dragOffset)
+            }
+            .frame(height: trackHeight)
+        }
+        .frame(height: trackHeight)
+    }
+}

+ 5 - 0
Trio/Sources/Modules/QuickBolusConfig/QuickBolusConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum QuickBolusConfig {
+    enum Config {}
+}
+
+protocol QuickBolusConfigProvider {}

+ 3 - 0
Trio/Sources/Modules/QuickBolusConfig/QuickBolusConfigProvider.swift

@@ -0,0 +1,3 @@
+extension QuickBolusConfig {
+    final class Provider: BaseProvider, QuickBolusConfigProvider {}
+}

+ 15 - 0
Trio/Sources/Modules/QuickBolusConfig/QuickBolusConfigStateModel.swift

@@ -0,0 +1,15 @@
+import SwiftUI
+
+extension QuickBolusConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Published var enableQuickBolus: Bool = false
+
+        override func subscribe() {
+            subscribeSetting(\.enableQuickBolus, on: $enableQuickBolus) { enableQuickBolus = $0 }
+        }
+    }
+}
+
+extension QuickBolusConfig.StateModel: SettingsObserver {
+    func settingsDidChange(_: TrioSettings) {}
+}

+ 64 - 0
Trio/Sources/Modules/QuickBolusConfig/View/QuickBolusConfigRootView.swift

@@ -0,0 +1,64 @@
+import SwiftUI
+import Swinject
+
+extension QuickBolusConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+
+        @StateObject var state = StateModel()
+
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: AnyView?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            List {
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.enableQuickBolus,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "Enable Quick Bolus")
+                        }
+                    ),
+                    units: .mgdL,
+                    type: .boolean,
+                    label: String(localized: "Enable Quick Bolus"),
+                    miniHint: String(localized: "Long-press the + button on the home screen to enact a quick bolus."),
+                    verboseHint: VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: OFF").bold()
+                        Text(
+                            "When enabled, long-pressing the + button on the home screen opens a Quick Bolus sheet. It suggests up to three bolus amounts based on your bolus history at similar times of day, weighted by recency and day type (weekday vs. weekend)."
+                        )
+                        Text(
+                            "Slide to confirm your selected amount. Face ID or Touch ID is always required before the bolus is enacted."
+                        )
+                    }
+                )
+            }
+            .listSectionSpacing(sectionSpacing)
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                    sheetTitle: String(localized: "Help", comment: "Help sheet title")
+                )
+            }
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Quick Bolus")
+            .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
+        }
+    }
+}

+ 1 - 0
Trio/Sources/Modules/Settings/View/Subviews/FeatureSettingsView.swift

@@ -22,6 +22,7 @@ struct FeatureSettingsView: BaseView {
                 header: Text("Trio Features"),
                 content: {
                     Text("Bolus Calculator").navigationLink(to: .bolusCalculatorConfig, from: self)
+                    Text("Quick Bolus").navigationLink(to: .quickBolusConfig, from: self)
                     Text("Meal Settings").navigationLink(to: .mealSettings, from: self)
                     Text("Shortcuts").navigationLink(to: .shortcutsConfig, from: self)
                     Text("Remote Control").navigationLink(to: .remoteControlConfig, from: self)

+ 3 - 0
Trio/Sources/Router/Screen.swift

@@ -29,6 +29,7 @@ enum Screen: Identifiable, Hashable {
     case watch
     case userInterfaceSettings
     case bolusCalculatorConfig
+    case quickBolusConfig
     case dynamicISF
     case calibrations
     case shortcutsConfig
@@ -123,6 +124,8 @@ extension Screen {
             UserInterfaceSettings.RootView(resolver: resolver)
         case .bolusCalculatorConfig:
             BolusCalculatorConfig.RootView(resolver: resolver)
+        case .quickBolusConfig:
+            QuickBolusConfig.RootView(resolver: resolver)
         case .dynamicISF:
             DynamicSettings.RootView(resolver: resolver)
         case .calibrations: