Просмотр исходного кода

refactor storeGlucose func to notify moc if it has changed, add Observer in home state and update chart glucose dots if changes occur

polscm32 2 лет назад
Родитель
Сommit
a5fa4ed025

+ 23 - 7
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -15,6 +15,7 @@ protocol GlucoseStorage {
     func nightscoutCGMStateNotUploaded() -> [NigtscoutTreatment]
     func nightscoutManualGlucoseNotUploaded() -> [NigtscoutTreatment]
     var alarm: GlucoseAlarm? { get }
+    func fetchGlucose() -> [GlucoseStored]
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -46,7 +47,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     func storeGlucose(_ glucose: [BloodGlucose]) {
         processQueue.sync {
-            debug(.deviceManager, "start storage glucose")
+            debug(.deviceManager, "Start storage of glucose data")
 
             self.coredataContext.perform {
                 let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
@@ -68,6 +69,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
                 var filteredGlucose = glucose.filter { !existingDates.contains($0.dateString) }
 
+                // prepare batch insert
                 let batchInsert = NSBatchInsertRequest(entity: GlucoseStored.entity(), dictionaryHandler: { (dict) -> Bool in
                     guard !filteredGlucose.isEmpty else {
                         return true // Stop if there are no more items
@@ -79,12 +81,26 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     dict["direction"] = glucoseEntry.direction?.symbol
                     return false // Continue processing
                 })
+                batchInsert.resultType = .objectIDs
 
+                // process batch insert and merge changes to context
                 do {
-                    try self.coredataContext.execute(batchInsert)
-                    debugPrint("Glucose Storage: saved glucose to core data")
+                    if let result = try self.coredataContext.execute(batchInsert) as? NSBatchInsertResult,
+                       let objectIDs = result.result as? [NSManagedObjectID]
+                    {
+                        // Merges the insertions into the context
+                        NSManagedObjectContext.mergeChanges(
+                            fromRemoteContextSave: [NSInsertedObjectsKey: objectIDs],
+                            into: [self.coredataContext]
+                        )
+                        debugPrint(
+                            "Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data and merged changes into coreDataContext"
+                        )
+                    }
                 } catch {
-                    debugPrint("Glucose Storage: failed to save glucose to core data: \(error)")
+                    debugPrint(
+                        "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert or merge changes: \(error)"
+                    )
                 }
 
                 debug(.deviceManager, "start storage cgmState")
@@ -183,9 +199,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     /// also tried this but here again you need to make everything asynchronous...
     ///  let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
     /// privateContext.parent = coredataContext /// merges changes to the core data context
-    private func fetchGlucose() -> [GlucoseStored] {
+    func fetchGlucose() -> [GlucoseStored] {
         do {
-            debugPrint("OpenAPS: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
+            debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
             return try coredataContext.fetch(GlucoseStored.fetch(
                 NSPredicate.predicateForOneDayAgo,
                 ascending: false,
@@ -193,7 +209,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 batchSize: 50
             ))
         } catch {
-            debugPrint("OpenAPS: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
+            debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
             return []
         }
     }

+ 1 - 0
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -16,4 +16,5 @@ protocol HomeProvider: Provider {
     func pumpReservoir() -> Decimal?
     func tempTarget() -> TempTarget?
     func announcement(_ hours: Int) -> [Announcement]
+    func fetchGlucose() -> [GlucoseStored]
 }

+ 4 - 0
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -67,5 +67,9 @@ extension Home {
             storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
                 ?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
         }
+
+        func fetchGlucose() -> [GlucoseStored] {
+            glucoseStorage.fetchGlucose()
+        }
     }
 }

+ 58 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -1,5 +1,6 @@
 import Combine
 import CoreData
+import Foundation
 import LoopKitUI
 import SwiftDate
 import SwiftUI
@@ -22,6 +23,7 @@ extension Home {
         @Published var autotunedBasalProfile: [BasalProfileEntry] = []
         @Published var basalProfile: [BasalProfileEntry] = []
         @Published var tempTargets: [TempTarget] = []
+        @Published var glucoseFromPersistence: [GlucoseStored] = []
         @Published var timerDate = Date()
         @Published var closedLoop = false
         @Published var pumpSuspended = false
@@ -68,6 +70,7 @@ extension Home {
         @Published var waitForSuggestion: Bool = false
 
         let context = CoreDataStack.shared.backgroundContext
+        private let backgroundQueue = DispatchQueue(label: "home_state.queue", qos: .background, attributes: .concurrent)
 
         override func subscribe() {
             setupBasals()
@@ -79,6 +82,8 @@ extension Home {
             setupReservoir()
             setupAnnouncements()
             setupCurrentPumpTimezone()
+            setupNotification()
+            updateGlucose()
 
             uploadStats = settingsManager.settings.uploadStats
             units = settingsManager.settings.units
@@ -191,6 +196,59 @@ extension Home {
                 .store(in: &lifetime)
         }
 
+        /// listens for the notifications sent when the managedObjectContext has changed
+        func setupNotification() {
+            Foundation.NotificationCenter.default.addObserver(
+                self,
+                selector: #selector(contextDidSave(_:)),
+                name: Notification.Name.NSManagedObjectContextObjectsDidChange,
+                object: context
+            )
+        }
+
+        /// determine the actions when the context has changed
+        ///
+        /// its done on a background thread and after that the UI gets updated on the main thread
+        @objc private func contextDidSave(_ notification: Notification) {
+            guard let userInfo = notification.userInfo else { return }
+
+            backgroundQueue.async { [weak self] in
+                self?.processUpdates(userInfo: userInfo)
+            }
+        }
+
+        private func processUpdates(userInfo: [AnyHashable: Any]) {
+            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+
+            let glucoseUpdates = objects.filter { $0 is GlucoseStored }
+
+            if glucoseUpdates.isNotEmpty {
+                updateGlucose()
+            }
+        }
+
+        ///wait for the fetch to complete and then update the UI on the main thread
+        private func updateGlucose() {
+            Task {
+                let results = await fetchGlucoseInBackground()
+                await MainActor.run {
+                    glucoseFromPersistence = results
+                }
+            }
+        }
+
+        ///do the heavy fetch operation in the background
+        private func fetchGlucoseInBackground() async -> [GlucoseStored] {
+            await withCheckedContinuation { continuation in
+                backgroundQueue.async {
+                    let results = self.provider.fetchGlucose()
+                    continuation.resume(returning: results)
+                }
+            }
+        }
+
         func runLoop() {
             provider.heartbeatNow()
         }

+ 3 - 2
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -66,7 +66,7 @@ struct MainChartView: View {
     @Binding var thresholdLines: Bool
     @Binding var isTempTargetActive: Bool
 
-    @StateObject var state = Home.StateModel()
+    @StateObject var state: Home.StateModel
 
     @State var didAppearTrigger = false
     @State private var BasalProfiles: [BasalProfile] = []
@@ -424,10 +424,11 @@ extension MainChartView {
         }
     }
 
+//
     private func drawGlucose() -> some ChartContent {
         /// glucose point mark
         /// filtering for high and low bounds in settings
-        ForEach(glucoseFromPersistence) { item in
+        ForEach(state.glucoseFromPersistence) { item in
             if smooth {
                 if item.glucose > Int(highGlucose) {
                     PointMark(

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -389,7 +389,7 @@ extension Home {
                     displayXgridLines: $state.displayXgridLines,
                     displayYgridLines: $state.displayYgridLines,
                     thresholdLines: $state.thresholdLines,
-                    isTempTargetActive: $state.isTempTargetActive
+                    isTempTargetActive: $state.isTempTargetActive, state: state
                 )
             }
             .padding(.bottom)