From f0b641a7f3d32e52e217a33690505836a6770da1 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 2 Nov 2026 14:36:46 -0600 Subject: [PATCH 1/2] Show the loop as open while a manual temp basal is running A manual temp basal stops automatic dosing, but the loop icon stayed closed and went stale and red while it ran, as if the loop were failing. Treat automation as active only when Closed Loop is on and no manual temp basal is running, for the status HUD, the loop status popup, the widget and Live Activity context and the watch. --- Loop/Managers/DeviceDataManager.swift | 4 ++++ Loop/Managers/ExtensionDataManager.swift | 2 +- Loop/Managers/WatchDataManager.swift | 2 +- Loop/View Controllers/StatusTableViewController.swift | 5 +++-- Loop/Views/LoopStatusModalView.swift | 4 +++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 710897147a..19db48210d 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1449,6 +1449,10 @@ extension DeviceDataManager: PumpManagerDeliveryDelegate { try await doseEnactor.enact(decisionId: decisionId, bolus: bolus, tempBasal: tempBasal, with: pumpManager) } + var isAutomaticDosingActive: Bool { + settingsManager.dosingEnabled && !isManualTempBasalRunning + } + var isPumpInoperable: Bool { guard let basalDeliveryState else { return true diff --git a/Loop/Managers/ExtensionDataManager.swift b/Loop/Managers/ExtensionDataManager.swift index 915da7554b..35aec58f76 100644 --- a/Loop/Managers/ExtensionDataManager.swift +++ b/Loop/Managers/ExtensionDataManager.swift @@ -135,7 +135,7 @@ final class ExtensionDataManager { context.mostRecentGlucoseDataDate = loopDataManager.mostRecentGlucoseDataDate context.mostRecentPumpDataDate = loopDataManager.mostRecentPumpDataDate - context.isClosedLoop = self.settingsManager.dosingEnabled + context.isClosedLoop = self.deviceManager.isAutomaticDosingActive context.preMealPresetAllowed = self.settingsManager.dosingEnabled && self.settingsManager.settings.preMealTargetRange != nil context.preMealPresetActive = self.temporaryPresetsManager.isPreMealTargetActive() diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 04fa25da94..c2c92ff569 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -299,7 +299,7 @@ final class WatchDataManager: NSObject { let settings = self.settingsManager.loopSettings - context.isClosedLoop = settings.dosingEnabled + context.isClosedLoop = deviceManager.isAutomaticDosingActive context.isOnboardingCompleted = deviceManager.cgmManager?.isOnboarded == true && deviceManager.pumpManager?.isOnboarded == true context.deviceIssue = deviceManager.cgmManager == nil || deviceManager.cgmManager?.isInoperable == true || deviceManager.cgmManager?.inSignalLoss == true || deviceManager.pumpManager == nil || deviceManager.pumpManager?.isInoperable == true || deviceManager.pumpManager?.inSignalLoss == true || deviceManager.hasBluetoothIssue diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 3551774a9b..ebafbb8778 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -377,6 +377,7 @@ final class StatusTableViewController: LoopChartsTableViewController { if oldValue != basalDeliveryState { Task { @MainActor in log.debug("New basalDeliveryState: %@", String(describing: basalDeliveryState)) + hudView?.loopCompletionHUD.loopIconClosed = deviceManager.isAutomaticDosingActive refreshContext.update(with: .status) await reloadData(animated: true) } @@ -1731,7 +1732,7 @@ final class StatusTableViewController: LoopChartsTableViewController { private func automaticDosingStatusChanged(_ automaticDosingEnabled: Bool) { log.debug("automaticDosingStatusChanged -> %{public}@", String(describing: automaticDosingEnabled)) - hudView?.loopCompletionHUD.loopIconClosed = automaticDosingEnabled + hudView?.loopCompletionHUD.loopIconClosed = deviceManager.isAutomaticDosingActive hudView?.loopCompletionHUD.closedLoopDisallowedLocalizedDescription = deviceManager.closedLoopDisallowedLocalizedDescription if automaticDosingEnabled { @@ -1765,7 +1766,7 @@ final class StatusTableViewController: LoopChartsTableViewController { // when HUD view is initialized, update loop completion HUD (e.g., icon and last loop completed) hudView.loopCompletionHUD.stateColors = .loopStatus - hudView.loopCompletionHUD.loopIconClosed = settingsManager.dosingEnabled + hudView.loopCompletionHUD.loopIconClosed = deviceManager.isAutomaticDosingActive hudView.loopCompletionHUD.lastLoopCompleted = loopManager.lastLoopCompleted hudView.loopCompletionHUD.mostRecentGlucoseDataDate = loopManager.mostRecentGlucoseDataDate hudView.loopCompletionHUD.mostRecentPumpDataDate = loopManager.mostRecentPumpDataDate diff --git a/Loop/Views/LoopStatusModalView.swift b/Loop/Views/LoopStatusModalView.swift index b9926c57ea..7d0f376253 100644 --- a/Loop/Views/LoopStatusModalView.swift +++ b/Loop/Views/LoopStatusModalView.swift @@ -163,7 +163,7 @@ class LoopStatusModalViewModel { } var loopIconClosed: Bool { - settingsManager?.dosingEnabled ?? true + deviceManager?.isAutomaticDosingActive ?? settingsManager?.dosingEnabled ?? true } var hasBluetoothIssue: Bool { @@ -246,6 +246,8 @@ class LoopStatusModalViewModel { return (titleDeviceIssue, NSLocalizedString("Tap your CGM or insulin pump status icons right away for more information and steps to resolve the issue.", comment: "message when automation is off and there is a bluetooth or pump issue")) } else if isDeliverySuspended { return (titleAutomationOff, NSLocalizedString("Resume insulin if you wish for the app to restart insulin delivery.\n\nIf you wish for the app to automate your insulin, go to Settings and toggle Closed Loop to on.", comment: "message when automation is off and insulin delivery is suspended")) + } else if isManualTempBasalRunning, settingsManager?.dosingEnabled == true { + return (titleAutomationOff, NSLocalizedString("Your pump and CGM will continue to operate, but the app will not adjust insulin dosing while a manual temporary basal is in progress.\n\nCancel the manual temporary basal if you wish for the app to restart automated dosing.", comment: "message when closed loop is on and a manual temp basal is in progress")) } else if isManualTempBasalRunning { return (titleAutomationOff, NSLocalizedString("Cancel the manual temporary basal if you wish for the app to restart automated dosing.\n\nIf you wish for the app to automate your insulin, go to Settings and toggle Closed Loop to on.", comment: "message when automation is off and a manual temp basal is in progress")) } else if isCGMInoperable { From a0a5a72f858b3a19ddc2beaebf5ea67880f97676 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 2 Nov 2026 20:25:13 -0600 Subject: [PATCH 2/2] Don't send Loop Failure notifications while a manual temp basal is running A manual temp basal blocks enacting, so lastLoopCompleted stops advancing and the 20/40/60/120 minute Loop Failure notifications fired even though the loop was still running. While a manual temp basal runs, schedule them from each loop cycle instead, so they still fire if the loop stops. --- Loop/Managers/Alerts/AlertManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Loop/Managers/Alerts/AlertManager.swift b/Loop/Managers/Alerts/AlertManager.swift index 99c4cf9501..e679a82e56 100644 --- a/Loop/Managers/Alerts/AlertManager.swift +++ b/Loop/Managers/Alerts/AlertManager.swift @@ -111,7 +111,8 @@ public final class AlertManager { .sink { [weak self] publisher in if let loopDataManager = publisher.object as? LoopDataManager { Task { @MainActor in - await self?.loopDidComplete(loopDataManager.lastLoopCompleted) + let manualTempBasalRunning = loopDataManager.deliveryDelegate?.isManualTempBasalRunning == true + await self?.loopDidComplete(manualTempBasalRunning ? nil : loopDataManager.lastLoopCompleted) } } }