Kaynağa Gözat

Merge branch 'core-data-sync-trio' of github.com:nightscout/Trio-dev into watch

Deniz Cengiz 1 yıl önce
ebeveyn
işleme
855e43645f

+ 19 - 9
Model/CoreDataObserver.swift

@@ -2,24 +2,34 @@ import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 
 
-func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObject>, Never> {
+func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObjectID>, Never> {
     Foundation.NotificationCenter.default
     Foundation.NotificationCenter.default
         .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
         .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
         .map { notification in
         .map { notification in
-            guard let userInfo = notification.userInfo else { return Set<NSManagedObject>() }
+            guard let userInfo = notification.userInfo else { return Set<NSManagedObjectID>() }
 
 
-            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            var objectIDs = Set<NSManagedObjectID>()
 
 
-            return objects
+            if let inserted = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(inserted.map(\.objectID))
+            }
+            if let updated = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(updated.map(\.objectID))
+            }
+            if let deleted = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(deleted.map(\.objectID))
+            }
+
+            return objectIDs
         }
         }
 }
 }
 
 
-extension Publisher where Output == Set<NSManagedObject> {
+extension Publisher where Output == Set<NSManagedObjectID> {
     func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
     func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
-        filter { objects in
-            objects.contains(where: { $0.entity.name == name })
+        filter { objectIDs in
+            objectIDs.contains { objectID in
+                objectID.entity.name == name
+            }
         }
         }
     }
     }
 }
 }

+ 2 - 2
Model/Helper/Determination+helper.swift

@@ -35,7 +35,7 @@ extension NSPredicate {
     static var enactedDeterminationsNotYetUploadedToNightscout: NSPredicate {
     static var enactedDeterminationsNotYetUploadedToNightscout: NSPredicate {
         NSPredicate(
         NSPredicate(
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND enacted == %@",
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND enacted == %@",
-            Date.sixHoursAgo as NSDate,
+            Date.oneDayAgo as NSDate,
             false as NSNumber,
             false as NSNumber,
             true as NSNumber
             true as NSNumber
         )
         )
@@ -44,7 +44,7 @@ extension NSPredicate {
     static var suggestedDeterminationsNotYetUploadedToNightscout: NSPredicate {
     static var suggestedDeterminationsNotYetUploadedToNightscout: NSPredicate {
         NSPredicate(
         NSPredicate(
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND (enacted == %@ OR enacted == nil OR enacted != %@)",
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND (enacted == %@ OR enacted == nil OR enacted != %@)",
-            Date.sixHoursAgo as NSDate,
+            Date.oneDayAgo as NSDate,
             false as NSNumber,
             false as NSNumber,
             true as NSNumber,
             true as NSNumber,
             true as NSNumber
             true as NSNumber

+ 6 - 12
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -282,8 +282,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         return await coredataContext.perform {
         return await coredataContext.perform {
@@ -314,8 +313,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -369,8 +367,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -400,8 +397,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -431,8 +427,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -463,8 +458,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }

+ 3 - 6
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -290,8 +290,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
             key: "timestamp",
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         return await context.perform { [self] in
         return await context.perform { [self] in
@@ -450,8 +449,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
             key: "timestamp",
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
@@ -493,8 +491,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             key: "timestamp",
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }

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

@@ -119,7 +119,9 @@ extension Home {
         let batteryFetchContext = CoreDataStack.shared.newTaskContext()
         let batteryFetchContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
-        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        // Queue for handling Core Data change notifications
+        private let queue = DispatchQueue(label: "HomeStateModel.queue", qos: .userInitiated)
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
         private var subscriptions = Set<AnyCancellable>()
         private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
@@ -127,7 +129,7 @@ extension Home {
         override func subscribe() {
         override func subscribe() {
             coreDataPublisher =
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
                 changedObjectsOnManagedObjectContextDidSavePublisher()
-                    .receive(on: DispatchQueue.global(qos: .background))
+                    .receive(on: queue)
                     .share()
                     .share()
                     .eraseToAnyPublisher()
                     .eraseToAnyPublisher()
 
 

+ 1 - 1
Trio/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -201,7 +201,7 @@ extension MainChartView {
                 domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue
                 domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue
                     .asMmolL ... state.maxYAxisValue.asMmolL
                     .asMmolL ... state.maxYAxisValue.asMmolL
             )
             )
-            .backport.chartForegroundStyleScale(state: state)
+            .chartLegend(.hidden)
         }
         }
     }
     }
 }
 }

+ 5 - 4
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -122,7 +122,9 @@ extension Treatments {
 
 
         var isActive: Bool = false
         var isActive: Bool = false
 
 
-        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        // Queue for handling Core Data change notifications
+        private let queue = DispatchQueue(label: "TreatmentsStateModel.queue", qos: .userInitiated)
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
         private var subscriptions = Set<AnyCancellable>()
         private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
@@ -143,7 +145,7 @@ extension Treatments {
             debug(.bolusState, "subscribe fired")
             debug(.bolusState, "subscribe fired")
             coreDataPublisher =
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
                 changedObjectsOnManagedObjectContextDidSavePublisher()
-                    .receive(on: DispatchQueue.global(qos: .background))
+                    .receive(on: queue)
                     .share()
                     .share()
                     .eraseToAnyPublisher()
                     .eraseToAnyPublisher()
             registerHandlers()
             registerHandlers()
@@ -683,8 +685,7 @@ extension Treatments.StateModel {
             onContext: glucoseFetchContext,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         return await glucoseFetchContext.perform {
         return await glucoseFetchContext.perform {

+ 22 - 1
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -163,7 +163,28 @@ struct ForecastChart: View {
         .chartXScale(domain: startMarker ... endMarker)
         .chartXScale(domain: startMarker ... endMarker)
         .chartYAxis { forecastChartYAxis }
         .chartYAxis { forecastChartYAxis }
         .chartYScale(domain: state.units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
         .chartYScale(domain: state.units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
-        .backport.chartForegroundStyleScale(state: state)
+        .chartLegend {
+            if state.forecastDisplayType == ForecastDisplayType.lines {
+                HStack(spacing: 10) {
+                    HStack(spacing: 4) {
+                        Image(systemName: "circle.fill").foregroundStyle(Color.insulin)
+                        Text("IOB").foregroundStyle(Color.secondary)
+                    }
+                    HStack(spacing: 4) {
+                        Image(systemName: "circle.fill").foregroundStyle(Color.uam)
+                        Text("UAM").foregroundStyle(Color.secondary)
+                    }
+                    HStack(spacing: 4) {
+                        Image(systemName: "circle.fill").foregroundStyle(Color.zt)
+                        Text("ZT").foregroundStyle(Color.secondary)
+                    }
+                    HStack(spacing: 4) {
+                        Image(systemName: "circle.fill").foregroundStyle(Color.orange)
+                        Text("COB").foregroundStyle(Color.secondary)
+                    }
+                }.font(.caption2)
+            }
+        }
     }
     }
 
 
     @ViewBuilder var selectionPopover: some View {
     @ViewBuilder var selectionPopover: some View {

+ 4 - 2
Trio/Sources/Services/Calendar/CalendarManager.swift

@@ -19,7 +19,9 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseCalendarManager.queue", qos: .background)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     private var glucoseFormatter: NumberFormatter {
     private var glucoseFormatter: NumberFormatter {
@@ -62,7 +64,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
 
 
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

+ 4 - 2
Trio/Sources/Services/ContactImage/ContactImageManager.swift

@@ -32,7 +32,9 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseContactImageManager.queue", qos: .background)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     private var units: GlucoseUnits = .mgdL
     private var units: GlucoseUnits = .mgdL
@@ -54,7 +56,7 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
         units = settingsManager.settings.units
         units = settingsManager.settings.units
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

+ 4 - 2
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -57,7 +57,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
 
 
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseHealthKitManager.queue", qos: .background)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     var isAvailableOnCurrentDevice: Bool {
     var isAvailableOnCurrentDevice: Bool {
@@ -69,7 +71,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
 
 
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

+ 14 - 3
Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -47,13 +47,16 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
 
     let context = CoreDataStack.shared.newTaskContext()
     let context = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
+    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 
@@ -103,7 +106,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
 
         coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
-            self.cobOrIobDidUpdate()
+            self.orefDeterminationSubject.send()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
     }
     }
 
 
@@ -115,6 +118,14 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
+
+        orefDeterminationSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                self.cobOrIobDidUpdate()
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     private func cobOrIobDidUpdate() {
     private func cobOrIobDidUpdate() {

+ 1 - 1
Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -363,7 +363,7 @@ extension NightscoutAPI {
 //        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
 //        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
     }
 
 
-    func uploadStatus(_ status: NightscoutStatus) async throws {
+    func uploadDeviceStatus(_ status: NightscoutStatus) async throws {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme
         components.host = url.host
         components.host = url.host

+ 148 - 65
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -12,7 +12,7 @@ protocol NightscoutManager: GlucoseSource {
     func deleteCarbs(withID id: String) async
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteManualGlucose(withID id: String) async
     func deleteManualGlucose(withID id: String) async
-    func uploadStatus() async
+    func uploadDeviceStatus() async
     func uploadGlucose() async
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadPumpHistory() async
@@ -39,6 +39,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() var healthkitManager: HealthKitManager!
     @Injected() var healthkitManager: HealthKitManager!
 
 
+    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
     private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
     private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
     private let uploadPumpHistorySubject = PassthroughSubject<Void, Never>()
     private let uploadPumpHistorySubject = PassthroughSubject<Void, Never>()
     private let uploadCarbsSubject = PassthroughSubject<Void, Never>()
     private let uploadCarbsSubject = PassthroughSubject<Void, Never>()
@@ -78,7 +79,9 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private var lastEnactedDetermination: Determination?
     private var lastEnactedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseNightscoutManager.queue", qos: .background)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
@@ -87,56 +90,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
 
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 
-        glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadGlucose()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadOverridesSubject
-            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadOverrides()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadPumpHistorySubject
-            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadPumpHistory()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadCarbsSubject
-            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadCarbs()
-                }
-            }
-            .store(in: &subscriptions)
-
+        registerSubscribers()
         registerHandlers()
         registerHandlers()
         setupNotification()
         setupNotification()
 
 
         /// Ensure that Nightscout Manager holds the `lastEnactedDetermination`, if one exists, on initialization.
         /// Ensure that Nightscout Manager holds the `lastEnactedDetermination`, if one exists, on initialization.
         /// We have to set this here in `init()`, so there's a `lastEnactedDetermination` available after an app restart
         /// We have to set this here in `init()`, so there's a `lastEnactedDetermination` available after an app restart
-        /// for `uploadStatus()`, as within that fuction `lastEnactedDetermination` is reassigned at the very end of the function.
+        /// for `uploadDeviceStatus()`, as within that fuction `lastEnactedDetermination` is reassigned at the very end of the function.
         /// This way, we ensure the latest enacted determination is always part of `devicestatus` and avoid having instances
         /// This way, we ensure the latest enacted determination is always part of `devicestatus` and avoid having instances
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         Task {
         Task {
@@ -155,12 +119,32 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadStatus()
+        coreDataPublisher?
+            .filterByEntityName("OrefDetermination")
+            .sink { [weak self] objectIDs in
+                guard let self = self else { return }
+
+                // Now hop onto the background context's queue
+                self.backgroundContext.perform {
+                    do {
+                        // Fetch only those determination objects
+                        let request: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
+                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
+                        let results = try self.backgroundContext.fetch(request)
+
+                        // Safely filter out anything that's deleted or already uploaded
+                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
+
+                        // If valid, proceed to send to subject for further processing
+                        if !unuploaded.isEmpty {
+                            self.orefDeterminationSubject.send()
+                        }
+                    } catch {
+                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                    }
+                }
             }
             }
-        }.store(in: &subscriptions)
+            .store(in: &subscriptions)
 
 
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             self?.uploadOverridesSubject.send()
             self?.uploadOverridesSubject.send()
@@ -184,20 +168,119 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
-            self?.uploadPumpHistorySubject.send()
-        }.store(in: &subscriptions)
+        coreDataPublisher?.filterByEntityName("PumpEventStored")
+            .sink { [weak self] objectIDs in
+                guard let self = self else { return }
 
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
-            self?.uploadCarbsSubject.send()
-        }.store(in: &subscriptions)
+                // Now hop onto the background context’s queue
+                self.backgroundContext.perform {
+                    do {
+                        let request: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
+                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
+                        let results = try self.backgroundContext.fetch(request)
+
+                        // Safely filter out anything that’s deleted or already uploaded
+                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
+
+                        // If valid, proceed to send to subject for further processing
+                        if !unuploaded.isEmpty {
+                            self.uploadPumpHistorySubject.send()
+                        }
+                    } catch {
+                        debugPrint("Failed to fetch PumpEventStored objects: \(error)")
+                    }
+                }
+            }
+            .store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadManualGlucose()
+        coreDataPublisher?.filterByEntityName("CarbEntryStored")
+            .sink { [weak self] objectIDs in
+                guard let self = self else { return }
+
+                // Now hop onto the background context’s queue
+                self.backgroundContext.perform {
+                    do {
+                        let request: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
+                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
+                        let results = try self.backgroundContext.fetch(request)
+
+                        // Safely filter out anything that’s deleted or already uploaded
+                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
+
+                        // If valid, proceed to send to subject for further processing
+                        if !unuploaded.isEmpty {
+                            self.uploadCarbsSubject.send()
+                        }
+                    } catch {
+                        debugPrint("Failed to fetch CarbEntryStored objects: \(error)")
+                    }
+                }
             }
             }
-        }.store(in: &subscriptions)
+            .store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("GlucoseStored")
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task.detached {
+                    await self.uploadManualGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+    }
+
+    func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+
+        /// We add debouncing behavior here for two main reasons
+        /// 1. To ensure that any upload flag updates have properly been performed, and in subsequent fetching processes only truly unuploaded data is fetched
+        /// 2. To not spam the user's NS site with a high number of uploads in a very short amount of time (less than 1sec)
+        orefDeterminationSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadDeviceStatus()
+                }
+            }
+            .store(in: &subscriptions)
+
+        uploadOverridesSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadOverrides()
+                }
+            }
+            .store(in: &subscriptions)
+
+        uploadPumpHistorySubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadPumpHistory()
+                }
+            }
+            .store(in: &subscriptions)
+
+        uploadCarbsSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadCarbs()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     func setupNotification() {
     func setupNotification() {
@@ -422,7 +505,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     ///
     ///
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Returns: Nothing.
     /// - Returns: Nothing.
-    func uploadStatus() async {
+    func uploadDeviceStatus() async {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             return
             return
@@ -531,15 +614,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         )
         )
 
 
         do {
         do {
-            try await nightscout.uploadStatus(status)
-            debug(.nightscout, "Status uploaded")
+            try await nightscout.uploadDeviceStatus(status)
+            debug(.nightscout, "NSDeviceStatus with Determination uploaded")
 
 
             if let enacted = fetchedEnactedDetermination {
             if let enacted = fetchedEnactedDetermination {
                 await updateOrefDeterminationAsUploaded([enacted])
                 await updateOrefDeterminationAsUploaded([enacted])
+                debug(.nightscout, "Flagged last fetched enacted determination as uploaded")
             }
             }
 
 
             if let suggested = fetchedSuggestedDetermination {
             if let suggested = fetchedSuggestedDetermination {
                 await updateOrefDeterminationAsUploaded([suggested])
                 await updateOrefDeterminationAsUploaded([suggested])
+                debug(.nightscout, "Flagged last fetched suggested determination as uploaded")
             }
             }
 
 
             if let lastEnactedDetermination = fetchedEnactedDetermination {
             if let lastEnactedDetermination = fetchedEnactedDetermination {
@@ -549,8 +634,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             if let lastSuggestedDetermination = fetchedSuggestedDetermination {
             if let lastSuggestedDetermination = fetchedSuggestedDetermination {
                 self.lastSuggestedDetermination = lastSuggestedDetermination
                 self.lastSuggestedDetermination = lastSuggestedDetermination
             }
             }
-
-            debug(.nightscout, "NSDeviceStatus with Determination uploaded")
         } catch {
         } catch {
             debug(.nightscout, error.localizedDescription)
             debug(.nightscout, error.localizedDescription)
         }
         }

+ 4 - 2
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -40,7 +40,9 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
 
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseTidepoolManager.queue", qos: .background)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
@@ -51,7 +53,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
 
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

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

@@ -65,7 +65,9 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseUserNotificationsManager.queue", qos: .userInitiated)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     let firstInterval = 20 // min
     let firstInterval = 20 // min
@@ -78,7 +80,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
 
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

+ 3 - 14
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -6,7 +6,6 @@ import UIKit
 import WatchConnectivity
 import WatchConnectivity
 
 
 /// Protocol defining the base functionality for Watch communication
 /// Protocol defining the base functionality for Watch communication
-// TODO: Complete this
 protocol WatchManager {
 protocol WatchManager {
     func setupWatchState() async -> WatchState
     func setupWatchState() async -> WatchState
 }
 }
@@ -33,7 +32,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     private var currentGlucoseTarget: Decimal = 100.0
     private var currentGlucoseTarget: Decimal = 100.0
     private var activeBolusAmount: Double = 0.0
     private var activeBolusAmount: Double = 0.0
 
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    // Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseWatchManagerManager.queue", qos: .utility)
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
 
 
     typealias PumpEvent = PumpEventStored.EventType
     typealias PumpEvent = PumpEventStored.EventType
@@ -905,18 +906,6 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
 
     /// Sends bolus progress updates to the Watch
     /// Sends bolus progress updates to the Watch
     /// - Parameter progress: The current bolus progress as a Decimal
     /// - Parameter progress: The current bolus progress as a Decimal
-//    private func sendBolusProgressToWatch(progress: Decimal?) async {
-//        guard let session = session, session.isReachable, let progress = progress else { return }
-//
-//        let message: [String: Any] = [
-//            WatchMessageKeys.bolusProgress: Double(truncating: progress as NSNumber),
-//            WatchMessageKeys.activeBolusAmount: activeBolusAmount
-//        ]
-//
-//        session.sendMessage(message, replyHandler: nil) { error in
-//            debug(.watchManager, "❌ Error sending bolus progress: \(error.localizedDescription)")
-//        }
-//    }
     private func sendBolusProgressToWatch(progress: Decimal?) async {
     private func sendBolusProgressToWatch(progress: Decimal?) async {
         guard let session = session, let progress = progress, let pumpManager = apsManager.pumpManager else { return }
         guard let session = session, let progress = progress, let pumpManager = apsManager.pumpManager else { return }
 
 

+ 4 - 1
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -88,8 +88,11 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Current glucose units, either mg/dL or mmol/L, read from user settings.
     /// Current glucose units, either mg/dL or mmol/L, read from user settings.
     private var units: GlucoseUnits = .mgdL
     private var units: GlucoseUnits = .mgdL
 
 
+    /// Queue for handling Core Data change notifications
+    private let queue = DispatchQueue(label: "BaseGarminManager.queue", qos: .utility)
+
     /// Publishes any changed CoreData objects that match our filters (e.g., OrefDetermination, GlucoseStored).
     /// Publishes any changed CoreData objects that match our filters (e.g., OrefDetermination, GlucoseStored).
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
 
 
     /// Additional local subscriptions (separate from `cancellables`) for CoreData events.
     /// Additional local subscriptions (separate from `cancellables`) for CoreData events.
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()

+ 0 - 25
Trio/Sources/Views/ViewModifiers.swift

@@ -169,28 +169,3 @@ extension View {
 struct Backport<Content: View> {
 struct Backport<Content: View> {
     let content: Content
     let content: Content
 }
 }
-
-extension Backport {
-    @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
-        if (state as? Treatments.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
-            (state as? Home.StateModel)?.forecastDisplayType == ForecastDisplayType.lines
-        {
-            let modifiedContent = content
-                .chartForegroundStyleScale([
-                    "iob": .blue,
-                    "uam": Color.uam,
-                    "zt": Color.zt,
-                    "cob": .orange
-                ])
-
-            if state is Home.StateModel {
-                modifiedContent
-                    .chartLegend(.hidden)
-            } else {
-                modifiedContent
-            }
-        } else {
-            content
-        }
-    }
-}