Quellcode durchsuchen

fix(notifications): standardize threading and fix/improve watch connectivity for snooze

- Standardize all snooze-related async work to Task { @MainActor [weak self] } instead of mixing DispatchQueue.main.async and Task patterns
- Fix/improve watch connectivity reliability: watch was sending snooze via transferUserInfo, but phone was expecting snooze in didReceiveMessage. Updated so watch uses sendMessage with transferUserInfo fallback for immediate delivery when reachable, queued when not and phone is ready to receive in both didReceiveMessage and didReceiveUserInfo.
- Enhance alert deduplication beyond original patch: round timestamps to minutes and add stable Core Data objectID fallback to prevent duplicate notifications
Charlie Chrisman vor 4 Monaten
Ursprung
Commit
904bb573cf

+ 15 - 1
Trio Watch App Extension/Helper/WatchNotificationHandler.swift

@@ -43,10 +43,24 @@ final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate
 
     /// Sends snooze request to iPhone via WatchConnectivity.
     /// WCSession.transferUserInfo is thread-safe and can be called from any thread.
+    /// Relies on the watch app's WCSession owner (e.g., WatchState) to handle
+    /// session activation and delegate management.
     private func sendSnoozeRequest(for action: NotificationResponseAction) {
         guard WCSession.isSupported() else { return }
 
         let payload: [String: Any] = [WatchMessageKeys.snoozeDuration: action.minutes]
-        WCSession.default.transferUserInfo(payload)
+        let session = WCSession.default
+
+        // Try sendMessage first if session is reachable and activated (faster, immediate delivery)
+        // Fall back to transferUserInfo if not reachable or if sendMessage fails
+        if session.isReachable && session.activationState == .activated {
+            session.sendMessage(payload, replyHandler: nil) { _ in
+                // Fallback to transferUserInfo if sendMessage fails
+                session.transferUserInfo(payload)
+            }
+        } else {
+            // Session not reachable or not activated - use transferUserInfo (queued delivery)
+            session.transferUserInfo(payload)
+        }
     }
 }

+ 2 - 1
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -688,7 +688,8 @@ extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
 
         // Handle quick snooze actions (from notification action buttons)
         if let quickAction = NotificationResponseAction(rawValue: response.actionIdentifier) {
-            Task { @MainActor in
+            Task { @MainActor [weak self] in
+                guard let self else { return }
                 await self.applySnooze(for: quickAction.duration)
             }
             return

+ 24 - 17
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -552,16 +552,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     }
 
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
-        DispatchQueue.main.async { [weak self] in
-            if let logs = message["watchLogs"] as? String {
-                SimpleLogReporter.appendToWatchLog(logs)
-            }
+        // Handle logs first - doesn't need self, so it can run even during teardown
+        if let logs = message["watchLogs"] as? String {
+            SimpleLogReporter.appendToWatchLog(logs)
+        }
+
+        Task { @MainActor [weak self] in
+            guard let self else { return }
 
             if let requestWatchUpdate = message[WatchMessageKeys.requestWatchUpdate] as? String,
                requestWatchUpdate == WatchMessageKeys.watchState
             {
                 debug(.watchManager, "📱 Watch requested watch state data update.")
-                guard let self = self else { return }
                 // Skip if no watch is paired or app not installed
                 guard let session = self.session, session.isPaired, session.isReachable,
                       session.isWatchAppInstalled else { return }
@@ -574,24 +576,21 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
             if let snoozeMinutes = message[WatchMessageKeys.snoozeDuration] as? Int {
                 debug(.watchManager, "📱 Received snooze request from watch: \(snoozeMinutes) minutes")
-                Task { @MainActor [weak self] in
-                    guard let self else { return }
-                    await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60))
-                }
+                await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60))
                 return
             } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
                       message[WatchMessageKeys.carbs] == nil,
                       message[WatchMessageKeys.date] == nil
             {
                 debug(.watchManager, "📱 Received bolus request from watch: \(bolusAmount)U")
-                self?.handleBolusRequest(Decimal(bolusAmount))
+                self.handleBolusRequest(Decimal(bolusAmount))
             } else if let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
                       let timestamp = message[WatchMessageKeys.date] as? TimeInterval,
                       message[WatchMessageKeys.bolus] == nil
             {
                 let date = Date(timeIntervalSince1970: timestamp)
                 debug(.watchManager, "📱 Received carbs request from watch: \(carbsAmount)g at \(date)")
-                self?.handleCarbsRequest(carbsAmount, date)
+                self.handleCarbsRequest(carbsAmount, date)
             } else if let bolusAmount = message[WatchMessageKeys.bolus] as? Double,
                       let carbsAmount = message[WatchMessageKeys.carbs] as? Int,
                       let timestamp = message[WatchMessageKeys.date] as? TimeInterval
@@ -601,11 +600,11 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     .watchManager,
                     "📱 Received meal bolus combo request from watch: \(bolusAmount)U, \(carbsAmount)g at \(date)"
                 )
-                self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
+                self.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
             } else {
                 debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received:  \(message)")
                 // Acknowledge failure
-                self?.sendAcknowledgment(
+                self.sendAcknowledgment(
                     toWatch: false,
                     message: "Error! Invalid or incomplete data received from watch.",
                     ackCode: .genericFailure
@@ -614,22 +613,22 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
             if message[WatchMessageKeys.cancelOverride] as? Bool == true {
                 debug(.watchManager, "📱 Received cancel override request from watch")
-                self?.handleCancelOverride()
+                self.handleCancelOverride()
             }
 
             if let presetName = message[WatchMessageKeys.activateOverride] as? String {
                 debug(.watchManager, "📱 Received activate override request from watch for preset: \(presetName)")
-                self?.handleActivateOverride(presetName)
+                self.handleActivateOverride(presetName)
             }
 
             if let presetName = message[WatchMessageKeys.activateTempTarget] as? String {
                 debug(.watchManager, "📱 Received activate temp target request from watch for preset: \(presetName)")
-                self?.handleActivateTempTarget(presetName)
+                self.handleActivateTempTarget(presetName)
             }
 
             if message[WatchMessageKeys.cancelTempTarget] as? Bool == true {
                 debug(.watchManager, "📱 Received cancel temp target request from watch")
-                self?.handleCancelTempTarget()
+                self.handleCancelTempTarget()
             }
 
             if message[WatchMessageKeys.requestBolusRecommendation] as? Bool == true {
@@ -690,6 +689,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         if let logs = userInfo["watchLogs"] as? String {
             SimpleLogReporter.appendToWatchLog(logs)
         }
+
+        if let snoozeMinutes = userInfo[WatchMessageKeys.snoozeDuration] as? Int {
+            debug(.watchManager, "📱 Received snooze userInfo from watch: \(snoozeMinutes) minutes")
+            Task { @MainActor [weak self] in
+                guard let self else { return }
+                await self.notificationsManager.applySnooze(for: TimeInterval(snoozeMinutes * 60))
+            }
+        }
     }
 
     #if os(iOS)