Bläddra i källkod

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Open-iAPS into adjust-treatment-view

Deniz Cengiz 1 år sedan
förälder
incheckning
12471a97c7

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -439,6 +439,7 @@
 		DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */; };
 		DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */; };
 		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
 		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
+		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
@@ -1093,6 +1094,7 @@
 		DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalInsulinDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalInsulinDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportResultView.swift; sourceTree = "<group>"; };
 		DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportResultView.swift; sourceTree = "<group>"; };
 		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
 		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
+		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
@@ -1893,6 +1895,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				38A00B1E25FC00F7006BC0B0 /* Autotune.swift */,
 				38A00B1E25FC00F7006BC0B0 /* Autotune.swift */,
@@ -3274,6 +3277,7 @@
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
+				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,

+ 58 - 2
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import SwiftDate
 import SwiftDate
@@ -8,7 +9,9 @@ protocol CarbsObserver {
 }
 }
 
 
 protocol CarbsStorage {
 protocol CarbsStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func recent() -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
@@ -24,6 +27,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
@@ -226,8 +235,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 try self.coredataContext.execute(batchInsert)
                 try self.coredataContext.execute(batchInsert)
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
 
 
-                // Send notification for triggering a fetch in Home State Model to update the FPU Array
-                Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
+                // Notify subscriber in Home State Model to update the FPU Array
+                self.updateSubject.send(())
             } catch {
             } catch {
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
             }
             }
@@ -242,6 +251,53 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
     }
 
 
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteCarbs"
+
+        var carbEntry: CarbEntryStored?
+
+        await taskContext.perform {
+            do {
+                carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                guard let carbEntry = carbEntry else {
+                    debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
+                    return
+                }
+
+                if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                    // fetch request for all carb entries with the same id
+                    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
+                    fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
+
+                    // NSBatchDeleteRequest
+                    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
+                    deleteRequest.resultType = .resultTypeCount
+
+                    // execute the batch delete request
+                    let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
+                    debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
+
+                    // Notifiy subscribers of the batch delete
+                    self.updateSubject.send(())
+                } else {
+                    taskContext.delete(carbEntry)
+
+                    guard taskContext.hasChanges else { return }
+                    try taskContext.save()
+
+                    debugPrint(
+                        "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
+                    )
+                }
+
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+            }
+        }
+    }
+
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
         processQueue.sync {
         processQueue.sync {
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []

+ 1 - 0
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject

+ 11 - 5
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -1,4 +1,5 @@
 import AVFAudio
 import AVFAudio
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import SwiftDate
 import SwiftDate
@@ -6,6 +7,7 @@ import SwiftUI
 import Swinject
 import Swinject
 
 
 protocol GlucoseStorage {
 protocol GlucoseStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeGlucose(_ glucose: [BloodGlucose])
     func storeGlucose(_ glucose: [BloodGlucose])
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
@@ -26,6 +28,12 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     private enum Config {
     private enum Config {
         static let filterTime: TimeInterval = 3.5 * 60
         static let filterTime: TimeInterval = 3.5 * 60
     }
     }
@@ -87,12 +95,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 // process batch insert
                 // process batch insert
                 do {
                 do {
                     try self.coredataContext.execute(batchInsert)
                     try self.coredataContext.execute(batchInsert)
-//                    debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
 
 
-                    // Send notification for triggering a fetch in Home State Model to update the Glucose Array
-                    /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
-                    /// But I do not want to fetch on the Main Thread using the @FetchRequest property, I also can not use the FetchedResultsController because of the architecture of the State Model (it must inherit from BaseStateModel and therefore can not inherit from NSObject as well) and because of the fact that I am using a batch insert here there are no notifications sent from the managedObjectContext because changes are directly stored in the persistent container
-                    Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
+                    // Notify subscribers that there is a new glucose value
+                    // We need to do this because the due to the batch insert there is no ManagedObjectContext notification
+                    self.updateSubject.send(())
                 } catch {
                 } catch {
                     debugPrint(
                     debugPrint(
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"

+ 12 - 0
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -9,6 +10,7 @@ protocol PumpHistoryObserver {
 }
 }
 
 
 protocol PumpHistoryStorage {
 protocol PumpHistoryStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storePumpEvents(_ events: [NewPumpEvent])
     func storePumpEvents(_ events: [NewPumpEvent])
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func recent() -> [PumpHistoryEvent]
     func recent() -> [PumpHistoryEvent]
@@ -22,6 +24,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settings: SettingsManager!
     @Injected() private var settings: SettingsManager!
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
@@ -190,6 +198,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                 do {
                 do {
                     guard self.context.hasChanges else { return }
                     guard self.context.hasChanges else { return }
                     try self.context.save()
                     try self.context.save()
+
+                    self.updateSubject.send(())
                     debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
                     debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
                 } catch let error as NSError {
                 } catch let error as NSError {
                     debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
                     debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
@@ -218,6 +228,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             do {
             do {
                 guard self.context.hasChanges else { return }
                 guard self.context.hasChanges else { return }
                 try self.context.save()
                 try self.context.save()
+
+                self.updateSubject.send(())
             } catch {
             } catch {
                 print(error.localizedDescription)
                 print(error.localizedDescription)
             }
             }

+ 15 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -10,6 +10,9 @@ import Swinject
 
 
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
 
+    // Read the color scheme preference from UserDefaults; defaults to system default setting
+    @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
     let coreDataStack = CoreDataStack.shared
     let coreDataStack = CoreDataStack.shared
 
 
     // Dependencies Assembler
     // Dependencies Assembler
@@ -67,6 +70,7 @@ import Swinject
     var body: some Scene {
     var body: some Scene {
         WindowGroup {
         WindowGroup {
             Main.RootView(resolver: resolver)
             Main.RootView(resolver: resolver)
+                .preferredColorScheme(colorScheme(for: colorSchemePreference ?? .systemDefault) ?? nil)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environmentObject(Icons())
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)
                 .onOpenURL(perform: handleURL)
@@ -85,6 +89,17 @@ import Swinject
         }
         }
     }
     }
 
 
+    private func colorScheme(for colorScheme: ColorSchemeOption) -> ColorScheme? {
+        switch colorScheme {
+        case .systemDefault:
+            return nil // Uses the system theme.
+        case .light:
+            return .light
+        case .dark:
+            return .dark
+        }
+    }
+
     func scheduleDatabaseCleaning() {
     func scheduleDatabaseCleaning() {
         let request = BGAppRefreshTaskRequest(identifier: "com.openiaps.cleanup")
         let request = BGAppRefreshTaskRequest(identifier: "com.openiaps.cleanup")
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days

+ 15 - 0
FreeAPS/Sources/Models/ColorSchemeOption.swift

@@ -0,0 +1,15 @@
+enum ColorSchemeOption: String, JSON, CaseIterable, Identifiable {
+    var id: String { rawValue }
+
+    case systemDefault
+    case light
+    case dark
+
+    var displayName: String {
+        switch self {
+        case .systemDefault: return "System Default"
+        case .light: return "Light"
+        case .dark: return "Dark"
+        }
+    }
+}

+ 27 - 20
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -117,18 +118,28 @@ extension Bolus {
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let backgroundContext = CoreDataStack.shared.newTaskContext()
         let backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-        private var coreDataObserver: CoreDataObserver?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
+            coreDataPublisher =
+                changedObjectsOnManagedObjectContextDidSavePublisher()
+                    .receive(on: DispatchQueue.global(qos: .background))
+                    .share()
+                    .eraseToAnyPublisher()
+            setupBolusStateConcurrently()
+        }
+
+        private func setupBolusStateConcurrently() {
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
-                        self.setupGlucoseNotification()
+                        self.registerHandlers()
                     }
                     }
                     group.addTask {
                     group.addTask {
-                        self.registerHandlers()
+                        self.registerSubscribers()
                     }
                     }
                     group.addTask {
                     group.addTask {
                         self.setupGlucoseArray()
                         self.setupGlucoseArray()
@@ -303,7 +314,7 @@ extension Bolus {
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
         func calculateInsulin() -> Decimal {
-            let isfForCalculation = units == .mmolL ? isf.asMgdL : isf
+            let isfForCalculation = isf
 
 
             // insulin needed for the current blood glucose
             // insulin needed for the current blood glucose
             targetDifference = currentBG - target
             targetDifference = currentBG - target
@@ -561,33 +572,29 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
 extension Bolus.StateModel {
 extension Bolus.StateModel {
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.setupDeterminationsArray()
                 await self.setupDeterminationsArray()
                 await self.updateForecasts()
                 await self.updateForecasts()
             }
             }
-        }
+        }.store(in: &subscriptions)
 
 
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.setupGlucoseArray()
             self.setupGlucoseArray()
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        setupGlucoseArray()
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupGlucoseArray()
+            }
+            .store(in: &subscriptions)
     }
     }
 }
 }
 
 

+ 45 - 58
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -134,14 +134,15 @@ struct PopupView: View {
             Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
             Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
                 .gridCellAnchor(.leading)
                 .gridCellAnchor(.leading)
 
 
+            let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
             Text(
             Text(
-                state.isf.formatted() + " " + state.units
+                isf + " " + state.units
                     .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
                     .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
             ).gridCellAnchor(.leading)
             ).gridCellAnchor(.leading)
-            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+
+            let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
             Text(
             Text(
-                target
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                target +
                     " " + state.units.rawValue
                     " " + state.units.rawValue
             ).gridCellAnchor(.leading)
             ).gridCellAnchor(.leading)
         }
         }
@@ -149,28 +150,25 @@ struct PopupView: View {
 
 
     var calcGlucoseFirstRow: some View {
     var calcGlucoseFirstRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
-            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+            let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
+            let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
 
 
             Text("Glucose:").foregroundColor(.secondary)
             Text("Glucose:").foregroundColor(.secondary)
 
 
-            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
+            let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
+                .description
             let firstRow = currentBG
             let firstRow = currentBG
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-
                 + " - " +
                 + " - " +
                 target
                 target
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
                 + " = " +
                 + " = " +
                 targetDifference
                 targetDifference
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
 
 
             Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
             Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
                 .gridColumnAlignment(.leading)
                 .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
                 Text(
                 Text(
-                    self.insulinRounder(state.targetDifferenceInsulin).formatted()
+                    self.insulinFormatter(state.targetDifferenceInsulin)
                 )
                 )
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
@@ -180,24 +178,18 @@ struct PopupView: View {
 
 
     var calcGlucoseSecondRow: some View {
     var calcGlucoseSecondRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+            let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
             Text(
             Text(
                 currentBG
                 currentBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    " " +
+                    + " " +
                     state.units.rawValue
                     state.units.rawValue
             )
             )
 
 
-            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
-            let secondRow = targetDifference
-                .formatted(
-                    .number.grouping(.never).rounded()
-                        .precision(.fractionLength(fractionDigits))
-                )
-                + " / " +
-                (state.units == .mgdL ? state.isf : state.isf.asMmolL).formatted()
-                + " ≈ " +
-                self.insulinRounder(state.targetDifferenceInsulin).formatted()
+            let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
+                .description
+            let secondRow = targetDifference + " / " +
+                (state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description)
+                .description + " ≈ " + self.insulinFormatter(state.targetDifferenceInsulin)
 
 
             Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
             Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
 
 
@@ -221,13 +213,13 @@ struct PopupView: View {
             HStack {
             HStack {
                 Text("IOB:").foregroundColor(.secondary)
                 Text("IOB:").foregroundColor(.secondary)
                 Text(
                 Text(
-                    self.insulinRounder(state.iob).formatted()
+                    self.insulinFormatter(state.iob)
                 )
                 )
             }
             }
 
 
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
-            let iobFormatted = self.insulinRounder(state.iob).formatted()
+            let iobFormatted = self.insulinFormatter(state.iob)
             HStack {
             HStack {
                 Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
                 Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -253,14 +245,14 @@ struct PopupView: View {
                     + " / " +
                     + " / " +
                     state.carbRatio.formatted()
                     state.carbRatio.formatted()
                     + " ≈ " +
                     + " ≈ " +
-                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                    self.insulinFormatter(state.wholeCobInsulin)
             )
             )
             .foregroundColor(.secondary)
             .foregroundColor(.secondary)
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
                 Text(
                 Text(
-                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                    self.insulinFormatter(state.wholeCobInsulin)
                 )
                 )
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
@@ -283,25 +275,19 @@ struct PopupView: View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
             Text("Delta:").foregroundColor(.secondary)
             Text("Delta:").foregroundColor(.secondary)
 
 
-            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
+            let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
+
+            let fifteenMinInsulinFormatted = self.insulinFormatter(state.fifteenMinInsulin)
+
             Text(
             Text(
-                deltaBG
-                    .formatted(
-                        .number.grouping(.never).rounded()
-                            .precision(.fractionLength(fractionDigits))
-                    )
-                    + " / " +
-                    state.isf.formatted()
-                    + " ≈ " +
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
+                deltaBG + " / " + isf + " ≈ " + fifteenMinInsulinFormatted
             )
             )
             .foregroundColor(.secondary)
             .foregroundColor(.secondary)
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
-                Text(
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
-                )
+                Text(fifteenMinInsulinFormatted)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
                 .gridColumnAlignment(.trailing)
                 .gridColumnAlignment(.trailing)
@@ -310,13 +296,10 @@ struct PopupView: View {
 
 
     var calcDeltaFormulaRow: some View {
     var calcDeltaFormulaRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
             Text(
             Text(
                 deltaBG
                 deltaBG
-                    .formatted(
-                        .number.grouping(.never).rounded()
-                            .precision(.fractionLength(fractionDigits))
-                    ) + " " +
+                    + " " +
                     state.units.rawValue
                     state.units.rawValue
             )
             )
 
 
@@ -334,7 +317,7 @@ struct PopupView: View {
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
 
 
             HStack {
             HStack {
-                Text(self.insulinRounder(state.wholeCalc).formatted())
+                Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.gridColumnAlignment(.trailing)
             }.gridColumnAlignment(.trailing)
@@ -350,7 +333,7 @@ struct PopupView: View {
             Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
             HStack {
             HStack {
-                Text("+" + self.insulinRounder(state.superBolusInsulin).formatted())
+                Text("+" + self.insulinFormatter(state.superBolusInsulin))
                     .foregroundStyle(Color.loopRed)
                     .foregroundStyle(Color.loopRed)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.gridColumnAlignment(.trailing)
             }.gridColumnAlignment(.trailing)
@@ -379,7 +362,7 @@ struct PopupView: View {
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
                     // endif fatty meal is chosen
                     // endif fatty meal is chosen
 
 
-                    + Text(self.insulinRounder(state.wholeCalc).formatted())
+                    + Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
 
 
                     // if superbolus is chosen
                     // if superbolus is chosen
@@ -389,7 +372,7 @@ struct PopupView: View {
                     + Text(state.useSuperBolus ? " + " : "")
                     + Text(state.useSuperBolus ? " + " : "")
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
 
 
-                    + Text(state.useSuperBolus ? state.superBolusInsulin.formatted() : "")
+                    + Text(state.useSuperBolus ? self.insulinFormatter(state.superBolusInsulin) : "")
                     .foregroundColor(.loopRed)
                     .foregroundColor(.loopRed)
                     // endif superbolus is chosen
                     // endif superbolus is chosen
 
 
@@ -399,7 +382,7 @@ struct PopupView: View {
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
-                Text(self.insulinRounder(state.insulinCalculated).formatted())
+                Text(self.insulinFormatter(state.insulinCalculated))
                     .fontWeight(.bold)
                     .fontWeight(.bold)
                     .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
                     .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -447,9 +430,17 @@ struct PopupView: View {
         }
         }
     }
     }
 
 
-    private func insulinRounder(_ value: Decimal) -> Decimal {
+    private func insulinFormatter(_ value: Decimal) -> String {
         let toRound = NSDecimalNumber(decimal: value).doubleValue
         let toRound = NSDecimalNumber(decimal: value).doubleValue
-        return Decimal(floor(100 * toRound) / 100)
+        let roundedValue = Decimal(floor(100 * toRound) / 100)
+
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumFractionDigits = 2
+        formatter.maximumFractionDigits = 2
+        formatter.locale = Locale.current // Uses the user's locale
+
+        return formatter.string(from: roundedValue as NSNumber) ?? String(format: "%.2f", toRound)
     }
     }
 
 
     struct DividerDouble: View {
     struct DividerDouble: View {
@@ -476,7 +467,3 @@ struct PopupView: View {
         }
         }
     }
     }
 }
 }
-
-// #Preview {
-//    PopupView()
-// }

+ 8 - 25
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -10,6 +10,7 @@ extension DataTable {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var healthKitManager: HealthKitManager!
         @Injected() var healthKitManager: HealthKitManager!
+        @Injected() var carbsStorage: CarbsStorage!
 
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
@@ -97,6 +98,7 @@ extension DataTable {
 
 
             var carbEntry: CarbEntryStored?
             var carbEntry: CarbEntryStored?
 
 
+            // Delete carbs or FPUs from Nightscout
             await taskContext.perform {
             await taskContext.perform {
                 do {
                 do {
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
@@ -108,42 +110,23 @@ extension DataTable {
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                         // Delete FPUs from Nightscout
                         // Delete FPUs from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
-
-                        // fetch request for all carb entries with the same id
-                        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                        fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
-
-                        // NSBatchDeleteRequest
-                        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
-                        deleteRequest.resultType = .resultTypeCount
-
-                        // execute the batch delete request
-                        let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
-                        debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
-
-                        Foundation.NotificationCenter.default.post(name: .didPerformBatchDelete, object: nil)
                     } else {
                     } else {
                         // Delete carbs from Nightscout
                         // Delete carbs from Nightscout
                         if let id = carbEntry.id?.uuidString {
                         if let id = carbEntry.id?.uuidString {
                             self.provider.deleteCarbsFromNightscout(withID: id)
                             self.provider.deleteCarbsFromNightscout(withID: id)
                         }
                         }
-
-                        // Now delete carbs also from the Database
-                        taskContext.delete(carbEntry)
-
-                        guard taskContext.hasChanges else { return }
-                        try taskContext.save()
-
-                        debugPrint(
-                            "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
-                        )
                     }
                     }
 
 
                 } catch {
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+                    debugPrint(
+                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from Nightscout: \(error.localizedDescription)"
+                    )
                 }
                 }
             }
             }
 
 
+            // Delete carbs from Core Data
+            await carbsStorage.deleteCarbs(treatmentObjectID)
+
             // Perform a determine basal sync to update cob
             // Perform a determine basal sync to update cob
             await apsManager.determineBasalSync()
             await apsManager.determineBasalSync()
         }
         }

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -70,7 +70,7 @@ extension DataTable {
                 formatter.minimumFractionDigits = 0
                 formatter.minimumFractionDigits = 0
                 formatter.maximumFractionDigits = 1
                 formatter.maximumFractionDigits = 1
             }
             }
-            formatter.roundingMode = .down
+            formatter.roundingMode = .halfUp
             return formatter
             return formatter
         }
         }
 
 

+ 42 - 49
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -13,6 +13,7 @@ extension Home {
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var determinationStorage: DeterminationStorage!
         @Injected() var determinationStorage: DeterminationStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var carbsStorage: CarbsStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         private(set) var filteredHours = 24
         @Published var manualGlucose: [BloodGlucose] = []
         @Published var manualGlucose: [BloodGlucose] = []
@@ -90,12 +91,17 @@ extension Home {
         let context = CoreDataStack.shared.newTaskContext()
         let context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
-        private var coreDataObserver: CoreDataObserver?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
-            coreDataObserver = CoreDataObserver()
+            coreDataPublisher =
+                changedObjectsOnManagedObjectContextDidSavePublisher()
+                    .receive(on: DispatchQueue.global(qos: .background))
+                    .share()
+                    .eraseToAnyPublisher()
 
 
             // Parallelize Setup functions
             // Parallelize Setup functions
             setupHomeViewConcurrently()
             setupHomeViewConcurrently()
@@ -105,7 +111,7 @@ extension Home {
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
-                        self.setupNotification()
+                        self.registerSubscribers()
                     }
                     }
                     group.addTask {
                     group.addTask {
                         self.registerHandlers()
                         self.registerHandlers()
@@ -165,43 +171,62 @@ extension Home {
             }
             }
         }
         }
 
 
+        // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
+        private func registerSubscribers() {
+            glucoseStorage.updatePublisher
+                .receive(on: DispatchQueue.global(qos: .background))
+                .sink { [weak self] _ in
+                    guard let self = self else { return }
+                    self.setupGlucoseArray()
+                }
+                .store(in: &subscriptions)
+
+            carbsStorage.updatePublisher
+                .receive(on: DispatchQueue.global(qos: .background))
+                .sink { [weak self] _ in
+                    guard let self = self else { return }
+                    self.setupFPUsArray()
+                }
+                .store(in: &subscriptions)
+        }
+
         private func registerHandlers() {
         private func registerHandlers() {
-            coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupDeterminationsArray()
                 self.setupDeterminationsArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupCarbsArray()
                 self.setupCarbsArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupInsulinArray()
                 self.setupInsulinArray()
                 self.setupLastBolus()
                 self.setupLastBolus()
                 self.displayPumpStatusHighlightMessage()
                 self.displayPumpStatusHighlightMessage()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "OpenAPS_Battery") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OpenAPS_Battery").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupBatteryArray()
                 self.setupBatteryArray()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupOverrides()
                 self.setupOverrides()
-            }
+            }.store(in: &subscriptions)
 
 
-            coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
+            coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupOverrideRunStored()
                 self.setupOverrideRunStored()
-            }
+            }.store(in: &subscriptions)
         }
         }
 
 
         private func registerObservers() {
         private func registerObservers() {
@@ -564,38 +589,6 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
     }
     }
 }
 }
 
 
-// MARK: - Setup Core Data observation
-
-extension Home.StateModel {
-    /// listens for the notifications sent when the managedObjectContext has saved!
-    func setupNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-
-        /// custom notification that is sent when a batch delete of fpus is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchDelete),
-            name: .didPerformBatchDelete,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        setupFPUsArray()
-        setupGlucoseArray()
-    }
-
-    @objc private func handleBatchDelete() {
-        setupFPUsArray()
-    }
-}
-
 // MARK: - Handle Core Data changes and update Arrays to display them in the UI
 // MARK: - Handle Core Data changes and update Arrays to display them in the UI
 
 
 extension Home.StateModel {
 extension Home.StateModel {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 47 - 47
FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift


+ 40 - 0
FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -15,6 +15,8 @@ extension UserInterfaceSettings {
         @State private var displayPickerLowThreshold: Bool = false
         @State private var displayPickerLowThreshold: Bool = false
         @State private var displayPickerHighThreshold: Bool = false
         @State private var displayPickerHighThreshold: Bool = false
 
 
+        @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
         var color: LinearGradient {
             colorScheme == .dark ? LinearGradient(
             colorScheme == .dark ? LinearGradient(
@@ -54,6 +56,44 @@ extension UserInterfaceSettings {
         var body: some View {
         var body: some View {
             Form {
             Form {
                 Section(
                 Section(
+                    header: Text("General Appearance"),
+                    content: {
+                        VStack {
+                            Picker(
+                                selection: $colorSchemePreference,
+                                label: Text("Color Scheme")
+                            ) {
+                                ForEach(ColorSchemeOption.allCases) { selection in
+                                    Text(selection.displayName).tag(selection)
+                                }
+                            }.padding(.top)
+
+                            HStack(alignment: .top) {
+                                Text(
+                                    "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                )
+                                .font(.footnote)
+                                .foregroundColor(.secondary)
+                                .lineLimit(nil)
+                                Spacer()
+                                Button(
+                                    action: {
+                                        hintLabel = "Color Scheme Preference"
+                                        selectedVerboseHint = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                        shouldDisplayHint.toggle()
+                                    },
+                                    label: {
+                                        HStack {
+                                            Image(systemName: "questionmark.circle")
+                                        }
+                                    }
+                                ).buttonStyle(BorderlessButtonStyle())
+                            }.padding(.top)
+                        }.padding(.bottom)
+                    }
+                ).listRowBackground(Color.chart)
+
+                Section(
                     header: Text("Home View Settings"),
                     header: Text("Home View Settings"),
                     content: {
                     content: {
                         VStack {
                         VStack {

+ 22 - 19
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -19,7 +19,8 @@ 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 coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     private var glucoseFormatter: NumberFormatter {
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
@@ -58,12 +59,18 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         setupCurrentCalendar()
         setupCurrentCalendar()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         Task {
         Task {
             await createEvent()
             await createEvent()
         }
         }
-        coreDataObserver = CoreDataObserver()
         registerHandlers()
         registerHandlers()
-        setupGlucoseNotification()
+        registerSubscribers()
     }
     }
 
 
     let backgroundContext = CoreDataStack.shared.newTaskContext()
     let backgroundContext = CoreDataStack.shared.newTaskContext()
@@ -79,28 +86,24 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.createEvent()
                 await self.createEvent()
             }
             }
-        }
-    }
-
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
+        }.store(in: &subscriptions)
     }
     }
 
 
-    @objc private func handleBatchInsert() {
-        Task {
-            await createEvent()
-        }
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.createEvent()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     func requestAccessIfNeeded() async -> Bool {
     func requestAccessIfNeeded() async -> Bool {

+ 21 - 7
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -1,4 +1,5 @@
 import ActivityKit
 import ActivityKit
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject
@@ -29,6 +30,7 @@ import UIKit
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
+    @Injected() private var glucoseStorage: GlucoseStorage!
 
 
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
     @Published private(set) var systemEnabled: Bool
     @Published private(set) var systemEnabled: Bool
@@ -45,13 +47,20 @@ import UIKit
 
 
     let context = CoreDataStack.shared.newTaskContext()
     let context = CoreDataStack.shared.newTaskContext()
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
         systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
         injectServices(resolver)
         injectServices(resolver)
         setupNotifications()
         setupNotifications()
-        coreDataObserver = CoreDataObserver()
+        registerSubscribers()
         registerHandler()
         registerHandler()
         monitorForLiveActivityAuthorizationChanges()
         monitorForLiveActivityAuthorizationChanges()
         setupGlucoseArray()
         setupGlucoseArray()
@@ -59,7 +68,6 @@ import UIKit
 
 
     private func setupNotifications() {
     private func setupNotifications() {
         let notificationCenter = Foundation.NotificationCenter.default
         let notificationCenter = Foundation.NotificationCenter.default
-        notificationCenter.addObserver(self, selector: #selector(handleBatchInsert), name: .didPerformBatchInsert, object: nil)
         notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
         notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
         notificationCenter
         notificationCenter
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
@@ -73,14 +81,20 @@ import UIKit
 
 
     private func registerHandler() {
     private func registerHandler() {
         // Since we are only using this info to show if an Override is active or not in the Live Activity it is enough to observe only the 'OverrideStored' Entity
         // Since we are only using this info to show if an Override is active or not in the Live Activity it is enough to observe only the 'OverrideStored' Entity
-        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.overridesDidUpdate()
             self.overridesDidUpdate()
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
-    @objc private func handleBatchInsert() {
-        setupGlucoseArray()
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupGlucoseArray()
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     @objc private func cobOrIobDidUpdate() {
     @objc private func cobOrIobDidUpdate() {

+ 26 - 14
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -72,12 +72,19 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private var lastEnactedDetermination: Determination?
     private var lastEnactedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
         subscribe()
         subscribe()
-        coreDataObserver = CoreDataObserver()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         registerHandlers()
         registerHandlers()
         setupNotification()
         setupNotification()
     }
     }
@@ -91,42 +98,47 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadStatus()
                 await self.uploadStatus()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadOverrides()
                 await self.uploadOverrides()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "OverrideRunStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadOverrides()
                 await self.uploadOverrides()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "PumpEventStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadPumpHistory()
                 await self.uploadPumpHistory()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "CarbEntryStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadCarbs()
                 await self.uploadCarbs()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task.detached {
             Task.detached {
                 await self.uploadManualGlucose()
                 await self.uploadManualGlucose()
             }
             }
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
     func setupNotification() {
     func setupNotification() {

+ 23 - 18
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -1,4 +1,5 @@
 import AudioToolbox
 import AudioToolbox
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -56,12 +57,20 @@ 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 coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         super.init()
         super.init()
         center.delegate = self
         center.delegate = self
         injectServices(resolver)
         injectServices(resolver)
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(DeterminationObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(BolusFailureObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
         broadcaster.register(pumpNotificationObserver.self, observer: self)
@@ -70,7 +79,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             await sendGlucoseNotification()
             await sendGlucoseNotification()
         }
         }
         registerHandlers()
         registerHandlers()
-        setupGlucoseNotification()
+        registerSubscribers()
         subscribeOnLoop()
         subscribeOnLoop()
     }
     }
 
 
@@ -84,28 +93,24 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
 
     private func registerHandlers() {
     private func registerHandlers() {
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.sendGlucoseNotification()
                 await self.sendGlucoseNotification()
             }
             }
-        }
-    }
-
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
+        }.store(in: &subscriptions)
     }
     }
 
 
-    @objc private func handleBatchInsert() {
-        Task {
-            await sendGlucoseNotification()
-        }
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.sendGlucoseNotification()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     private func addAppBadge(glucose: Int?) {
     private func addAppBadge(glucose: Int?) {

+ 29 - 23
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject
@@ -17,6 +18,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var garmin: GarminManager!
     @Injected() private var garmin: GarminManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
 
 
     private var glucoseFormatter: NumberFormatter {
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
@@ -56,7 +58,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     let context = CoreDataStack.shared.newTaskContext()
     let context = CoreDataStack.shared.newTaskContext()
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
-    private var coreDataObserver: CoreDataObserver?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     private var lifetime = Lifetime()
     private var lifetime = Lifetime()
 
 
@@ -64,9 +67,14 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         self.session = session
         self.session = session
         super.init()
         super.init()
         injectServices(resolver)
         injectServices(resolver)
-        setupNotification()
-        coreDataObserver = CoreDataObserver()
         registerHandlers()
         registerHandlers()
+        registerSubscribers()
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
 
 
         Task {
         Task {
             await configureState()
             await configureState()
@@ -94,42 +102,40 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         }
         }
     }
     }
 
 
-    func setupNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        Task {
-            await self.configureState()
-        }
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.configureState()
+                }
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.configureState()
                 await self.configureState()
             }
             }
-        }
-        coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.configureState()
                 await self.configureState()
             }
             }
-        }
+        }.store(in: &subscriptions)
+
         // Observes Deletion of Glucose Objects
         // Observes Deletion of Glucose Objects
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.configureState()
                 await self.configureState()
             }
             }
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
     private func fetchlastDetermination() async -> [NSManagedObjectID] {
     private func fetchlastDetermination() async -> [NSManagedObjectID] {

+ 1 - 1
LibreTransmitter

@@ -1 +1 @@
-Subproject commit c01eba63e94e9f6f2841a8835680c4e39c61b18d
+Subproject commit fb83de05e5505c8d114902077783c460e4ef23f0

+ 15 - 36
Model/CoreDataObserver.swift

@@ -1,46 +1,25 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 
 
-class CoreDataObserver {
-    private var entityUpdateHandlers: [String: () -> Void] = [:] // Dictionary to store pairs of entities and handlers
+func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObject>, Never> {
+    Foundation.NotificationCenter.default
+        .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
+        .map { notification in
+            guard let userInfo = notification.userInfo else { return Set<NSManagedObject>() }
 
 
-    init() {
-        setupNotification()
-    }
-
-    func registerHandler(for entityName: String, handler: @escaping () -> Void) {
-        entityUpdateHandlers[entityName] = handler
-    }
-
-    private func setupNotification() {
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(contextDidSave(_:)),
-            name: NSNotification.Name.NSManagedObjectContextDidSave,
-            object: nil
-        )
-    }
+            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
 
 
-    @objc private func contextDidSave(_ notification: Notification) {
-        guard let userInfo = notification.userInfo else { return }
-
-        Task {
-            await processUpdates(userInfo: userInfo)
+            return objects
         }
         }
-    }
-
-    private func processUpdates(userInfo: [AnyHashable: Any]) async {
-        var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-        objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-        objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+}
 
 
-        for (entityName, handler) in entityUpdateHandlers {
-            let entityUpdates = objects.filter { $0.entity.name == entityName }
-            DispatchQueue.global(qos: .background).async {
-                if entityUpdates.isNotEmpty {
-                    handler()
-                }
-            }
+extension Publisher where Output == Set<NSManagedObject> {
+    func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
+        filter { objects in
+            objects.contains(where: { $0.entity.name == name })
         }
         }
     }
     }
 }
 }

+ 0 - 4
Model/Helper/CustomNotification.swift

@@ -1,10 +1,6 @@
 import Foundation
 import Foundation
 
 
 extension Notification.Name {
 extension Notification.Name {
-    static let didPerformBatchInsert = Notification.Name("didPerformBatchInsert")
-    static let didPerformBatchUpdate = Notification.Name("didPerformBatchUpdate")
-    static let didPerformBatchDelete = Notification.Name("didPerformBatchDelete")
-    static let didUpdateDetermination = Notification.Name("didUpdateDetermination")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
     static let didUpdateCobIob = Notification.Name("didUpdateCobIob")
     static let didUpdateCobIob = Notification.Name("didUpdateCobIob")
 }
 }

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit eacf06f7873e73d6cb8ccd0556b35f734b90df40
+Subproject commit e39834584548821adf442f13abed0d5cfd237a72

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 03d3a1db5a4da9b218a60254fa1b0ea72ee808ed
+Subproject commit 849dc7abc821728dae7e064176a409e6ceb0dadd

+ 1 - 1
TidepoolService

@@ -1 +1 @@
-Subproject commit a2ccad72a55600c28549ab86ab1964c0d6558868
+Subproject commit b28625628e181b96f0db7ec3739d920a3c92465b