Преглед изворни кода

Pull in latest 'Core-data-sync-trio'

polscm32 пре 1 година
родитељ
комит
fa1868bba2
25 измењених фајлова са 425 додато и 322 уклоњено
  1. 4 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 43 48
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  3. 1 0
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  4. 11 5
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  5. 9 11
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  6. 15 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  7. 15 0
      FreeAPS/Sources/Models/ColorSchemeOption.swift
  8. 26 19
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  9. 1 0
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  10. 1 1
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  11. 42 49
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  12. 47 47
      FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  13. 40 0
      FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  14. 22 19
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  15. 30 17
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  16. 21 7
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  17. 26 14
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  18. 23 18
      FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift
  19. 29 23
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  20. 1 1
      LibreTransmitter
  21. 15 36
      Model/CoreDataObserver.swift
  22. 0 4
      Model/Helper/CustomNotification.swift
  23. 1 1
      OmniBLE
  24. 1 1
      OmniKit
  25. 1 1
      TidepoolService

+ 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 */,

+ 43 - 48
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -1,29 +1,23 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import SwiftDate
 import SwiftDate
 import Swinject
 import Swinject
 
 
-protocol CarbsStoredDelegate: AnyObject {
-    /*
-     Informs the delegate that the Carbs Storage has updated Carbs
-     */
-    func carbsStorageHasUpdatedCarbs(_ carbsStorage: BaseCarbsStorage)
-}
-
 protocol CarbsObserver {
 protocol CarbsObserver {
     func carbsDidUpdate(_ carbs: [CarbsEntry])
     func carbsDidUpdate(_ carbs: [CarbsEntry])
 }
 }
 
 
 protocol CarbsStorage {
 protocol CarbsStorage {
-    var delegate: CarbsStoredDelegate? { get set }
+    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]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
     func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
-    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
 }
 }
 
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -34,7 +28,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
-    public weak var delegate: CarbsStoredDelegate?
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
@@ -50,10 +48,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
 
 
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
-
-        // TODO: - Should we really use a delegate here? If yes, should we also use this for NS/TP?
-
-        delegate?.carbsStorageHasUpdatedCarbs(self)
     }
     }
 
 
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
     private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
@@ -250,8 +244,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")
             }
             }
@@ -266,36 +260,6 @@ 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(at uniqueID: String, fpuID: String, complex: Bool) {
-        processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
-
-            if fpuID != "" {
-                if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
-                    debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
-                } else {
-                    allValues.removeAll(where: { $0.fpuID == fpuID })
-                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                        $0.carbsDidUpdate(allValues)
-                    }
-                }
-            }
-
-            if fpuID == "" || complex {
-                if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
-                    debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
-                } else {
-                    allValues.removeAll(where: { $0.id == uniqueID })
-                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                        $0.carbsDidUpdate(allValues)
-                    }
-                }
-            }
-        }
-    }
-
     func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
     func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
         let taskContext = CoreDataStack.shared.newTaskContext()
         let taskContext = CoreDataStack.shared.newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.name = "deleteContext"
@@ -314,7 +278,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                 if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                     // fetch request for all carb entries with the same id
                     // fetch request for all carb entries with the same id
                     let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
                     let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                    fetchRequest.predicate = NSPredicate(format: "isFPU == true AND fpuID == %@", fpuID as CVarArg)
+                    fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
 
 
                     // NSBatchDeleteRequest
                     // NSBatchDeleteRequest
                     let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
                     let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
@@ -324,7 +288,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
                     let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
                     debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
                     debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
 
 
-                    Foundation.NotificationCenter.default.post(name: .didPerformBatchDelete, object: nil)
+                    // Notifiy subscribers of the batch delete
+                    self.updateSubject.send(())
                 } else {
                 } else {
                     taskContext.delete(carbEntry)
                     taskContext.delete(carbEntry)
 
 
@@ -342,6 +307,36 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
+    func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
+        processQueue.sync {
+            var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
+
+            if fpuID != "" {
+                if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
+                    debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
+                } else {
+                    allValues.removeAll(where: { $0.fpuID == fpuID })
+                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
+                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
+                        $0.carbsDidUpdate(allValues)
+                    }
+                }
+            }
+
+            if fpuID == "" || complex {
+                if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
+                    debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
+                } else {
+                    allValues.removeAll(where: { $0.id == uniqueID })
+                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
+                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
+                        $0.carbsDidUpdate(allValues)
+                    }
+                }
+            }
+        }
+    }
+
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.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
@@ -29,6 +31,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
     }
     }
@@ -91,12 +99,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)"

+ 9 - 11
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -1,22 +1,16 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
 import SwiftDate
 import SwiftDate
 import Swinject
 import Swinject
 
 
-protocol PumpHistoryDelegate: AnyObject {
-    /*
-     Informs the delegate that the Carbs Storage has updated Carbs
-     */
-    func pumpHistoryHasUpdated(_ pumpHistoryStorage: BasePumpHistoryStorage)
-}
-
 protocol PumpHistoryObserver {
 protocol PumpHistoryObserver {
     func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent])
     func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent])
 }
 }
 
 
 protocol PumpHistoryStorage {
 protocol PumpHistoryStorage {
-    var delegate: PumpHistoryDelegate? { get set }
+    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]
@@ -31,7 +25,11 @@ 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!
 
 
-    public weak var delegate: PumpHistoryDelegate?
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
@@ -210,7 +208,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     guard self.context.hasChanges else { return }
                     guard self.context.hasChanges else { return }
                     try self.context.save()
                     try self.context.save()
 
 
-                    self.delegate?.pumpHistoryHasUpdated(self)
+                    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)")
@@ -241,7 +239,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                 guard self.context.hasChanges else { return }
                 guard self.context.hasChanges else { return }
                 try self.context.save()
                 try self.context.save()
 
 
-                self.delegate?.pumpHistoryHasUpdated(self)
+                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"
+        }
+    }
+}

+ 26 - 19
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()
@@ -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)
     }
     }
 }
 }
 
 

+ 1 - 0
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -120,6 +120,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

+ 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 {

Разлика између датотеке није приказан због своје велике величине
+ 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 {

+ 30 - 17
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -46,7 +46,7 @@ public enum AppleHealthConfig {
     static let TrioMetaDataKey = "TrioMetaDataKey"
     static let TrioMetaDataKey = "TrioMetaDataKey"
 }
 }
 
 
-final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDelegate, PumpHistoryDelegate {
+final class BaseHealthKitManager: HealthKitManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var healthKitStore: HKHealthStore!
     @Injected() private var healthKitStore: HKHealthStore!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
@@ -56,19 +56,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
 
 
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
 
-    func carbsStorageHasUpdatedCarbs(_: BaseCarbsStorage) {
-        Task.detached { [weak self] in
-            guard let self = self else { return }
-            await self.uploadCarbs()
-        }
-    }
-
-    func pumpHistoryHasUpdated(_: BasePumpHistoryStorage) {
-        Task.detached { [weak self] in
-            guard let self = self else { return }
-            await self.uploadInsulin()
-        }
-    }
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var subscriptions = Set<AnyCancellable>()
 
 
     var isAvailableOnCurrentDevice: Bool {
     var isAvailableOnCurrentDevice: Bool {
         HKHealthStore.isHealthDataAvailable()
         HKHealthStore.isHealthDataAvailable()
@@ -76,15 +65,39 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsStoredDeleg
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
+
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: DispatchQueue.global(qos: .background))
+                .share()
+                .eraseToAnyPublisher()
+
+        registerHandlers()
+        
         guard isAvailableOnCurrentDevice,
         guard isAvailableOnCurrentDevice,
               AppleHealthConfig.healthBGObject != nil else { return }
               AppleHealthConfig.healthBGObject != nil else { return }
 
 
-        carbsStorage.delegate = self
-        pumpHistoryStorage.delegate = self
-
         debug(.service, "HealthKitManager did create")
         debug(.service, "HealthKitManager did create")
     }
     }
 
 
+    private func registerHandlers() {
+        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadInsulin()
+            }
+        }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+            guard let self = self else { return }
+            Task { [weak self] in
+                guard let self = self else { return }
+                await self.uploadCarbs()
+            }
+        }.store(in: &subscriptions)
+    }
+
     func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool {
     func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool {
         healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
         healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
     }
     }

+ 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