polscm32 1 год назад
Родитель
Сommit
128355097a

+ 4 - 0
Model/CoreDataStack.swift

@@ -376,6 +376,7 @@ extension CoreDataStack {
         fetchLimit: Int? = nil,
         batchSize: Int? = nil,
         propertiesToFetch: [String]? = nil,
+        relationshipKeyPathsForPrefetching: [String]? = nil,
         callingFunction: String = #function,
         callingClass: String = #fileID
     ) async -> Any {
@@ -394,6 +395,9 @@ extension CoreDataStack {
         } else {
             request.resultType = .managedObjectResultType
         }
+        if let prefetchKeyPaths = relationshipKeyPathsForPrefetching {
+            request.relationshipKeyPathsForPrefetching = prefetchKeyPaths
+        }
 
         context.name = "fetchContext"
         context.transactionAuthor = "fetchEntities"

+ 34 - 0
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -12,6 +12,8 @@ protocol DeterminationStorage {
         in context: NSManagedObjectContext
     ) async -> (UUID, Forecast?, [ForecastValue])
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
+    func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
+    async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
 }
 
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
@@ -202,4 +204,36 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             return result
         }
     }
+
+    func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
+    async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
+    {
+        // Fetch forecasts with prefetched values for the given determination
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: Forecast.self,
+            onContext: context,
+            predicate: NSPredicate(format: "orefDetermination = %@", determinationID),
+            key: "type",
+            ascending: true,
+            relationshipKeyPathsForPrefetching: ["forecastValues"]
+        )
+
+        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+        await context.perform {
+            if let forecasts = results as? [Forecast] {
+                for forecast in forecasts {
+                    // Use the helper property that already sorts by index
+                    let sortedValues = forecast.forecastValuesArray
+                    result.append((
+                        id: UUID(),
+                        forecastID: forecast.objectID,
+                        forecastValueIDs: sortedValues.map(\.objectID)
+                    ))
+                }
+            }
+        }
+
+        return result
+    }
 }

+ 29 - 60
Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -3,108 +3,77 @@ import Foundation
 
 extension Home.StateModel {
     // Asynchronously preprocess Forecast data in a background thread
-    func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
+    func preprocessForecastData() async -> [(
+        id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]
+    )] {
         // Get the Determination ID on the main context
-        guard let id = await viewContext.perform({ self.enactedAndNonEnactedDeterminations.first?.objectID }) else {
+        guard let determination = await viewContext.perform({
+            self.enactedAndNonEnactedDeterminations.first
+        }) else {
             return []
         }
 
-        // Get the Forecast IDs for the Determination ID
-        // Here we can safely use a background context since we are using the NSManagedObjectID
-        let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: taskContext)
-
-        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
-
-        // Use a task group to fetch Forecast VALUE IDs concurrently
-        await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
-            for forecastID in forecastIDs {
-                group.addTask {
-                    // Fetch forecast value IDs asynchronously (but outside of perform)
-                    let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
-                        for: forecastID,
-                        in: self.taskContext
-                    )
-                    return (UUID(), forecastID, forecastValueIDs)
-                }
-            }
-
-            // Collect the results from the task group
-            for await (uuid, forecastID, forecastValueIDs) in group {
-                result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
-            }
-        }
-
-        return result
+        // Fetch complete forecast hierarchy with prefetched values
+        return await determinationStorage.fetchForecastHierarchy(
+            for: determination.objectID,
+            in: taskContext
+        )
     }
 
     // Update forecast data and UI on the main thread
     @MainActor func updateForecastData() async {
-        // Preprocess forecast data on a background thread
         let forecastDataIDs = await preprocessForecastData()
 
-        // Use an Array of Int instead of ForecastValues to be able to pass values thread safe
         var allForecastValues = [[Int]]()
         var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
 
-        // Use a task group to fetch forecast values concurrently
-        await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
-            for data in forecastDataIDs {
-                group.addTask {
-                    await self.determinationStorage
-                        .fetchForecastObjects(
-                            for: data,
-                            in: self.viewContext
-                        ) // This directly returns NSManagedobjects on the Main Thread
+        // Process prefetched data directly
+        for data in forecastDataIDs {
+            if let forecast = try? viewContext.existingObject(with: data.forecastID) as? Forecast {
+                let values = data.forecastValueIDs.compactMap {
+                    try? viewContext.existingObject(with: $0) as? ForecastValue
                 }
-            }
-
-            // Collect the results from the task group
-            for await (id, forecast, forecastValues) in group {
-                guard let forecast = forecast, !forecastValues.isEmpty else { continue }
 
-                // Extract only the 'value' from ForecastValue on the main thread
-                let forecastValueInts = forecastValues
-                    .compactMap { Int($0.value) }
+                // Extract values for graph
+                let forecastValueInts = values.map { Int($0.value) }
                 allForecastValues.append(forecastValueInts)
-                preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
+
+                // Add data for further processing
+                preprocessedData.append(contentsOf: values.map {
+                    (id: data.id, forecast: forecast, forecastValue: $0)
+                })
             }
         }
 
-        // Update Array on the Main Thread
+        // Update UI-relevant data
         self.preprocessedData = preprocessedData
 
-        // Ensure there are forecast values to process
         guard !allForecastValues.isEmpty else {
             minForecast = []
             maxForecast = []
             return
         }
 
-        // Update minCount on the Main Thread
         minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
-
-        // Safely read minCount for use inside the detached task
         let localMinCount = minCount
 
         guard localMinCount > 0 else { return }
 
-        // Copy allForecastValues to a local constant for thread safety
-        let localAllForecastValues = allForecastValues
-
-        // Calculate min and max forecast values in a background task
+        // Calculate min/max values for graph
         let (minResult, maxResult) = await Task.detached {
             let minForecast = (0 ..< localMinCount).map { index in
-                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
+                allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
+                    .min() ?? 0
             }
 
             let maxForecast = (0 ..< localMinCount).map { index in
-                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
+                allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
+                    .max() ?? 0
             }
 
             return (minForecast, maxForecast)
         }.value
 
-        // Update the properties on the main thread
         minForecast = minResult
         maxForecast = maxResult
     }