Quellcode durchsuchen

Replace our custom Core Data publisher with a NSFetchedResultsController in HomeStateModel

Marvin Polscheit vor 5 Tagen
Ursprung
Commit
e68bf09010

+ 9 - 0
Model/NSFetchedResultsControllerDelegate.swift

@@ -0,0 +1,9 @@
+import CoreData
+
+final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
+    var onContentChange: (() -> Void)?
+
+    func controllerDidChangeContent(_: NSFetchedResultsController<any NSFetchRequestResult>) {
+        onContentChange?()
+    }
+}

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -354,6 +354,7 @@
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD10516D2DA986E1007C6D89 /* PulsingLogoAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */; };
+		BD136FAE2F6AC55F002217F9 /* NSFetchedResultsControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD136FAD2F6AC555002217F9 /* NSFetchedResultsControllerDelegate.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */; };
 		BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D872D42FBFB00412DEB /* BolusStatsView.swift */; };
@@ -1209,6 +1210,7 @@
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
 		BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulsingLogoAnimation.swift; sourceTree = "<group>"; };
+		BD136FAD2F6AC555002217F9 /* NSFetchedResultsControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchedResultsControllerDelegate.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMetricsView.swift; sourceTree = "<group>"; };
@@ -2816,6 +2818,7 @@
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			children = (
+				BD136FAD2F6AC555002217F9 /* NSFetchedResultsControllerDelegate.swift */,
 				3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
@@ -4751,6 +4754,7 @@
 				BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
 				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
+				BD136FAE2F6AC55F002217F9 /* NSFetchedResultsControllerDelegate.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				DDF691052DA2CA23008BF16C /* AppDiagnosticsStateModel.swift in Sources */,

+ 18 - 31
Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift

@@ -2,43 +2,30 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupBatteryArray() {
-        Task {
-            do {
-                let ids = try await self.fetchBattery()
-                let batteryObjects: [OpenAPS_Battery] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateBatteryArray(with: batteryObjects)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up battery array: \(error)"
-                )
+    @MainActor func setupBatteryController() {
+        batteryControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateBatteryFromController()
             }
         }
-    }
-
-    private func fetchBattery() async throws -> [NSManagedObjectID] {
-        let batteryFetchContext = CoreDataStack.shared.newTaskContext()
-        batteryFetchContext.name = "HomeStateModel.fetchBattery"
 
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OpenAPS_Battery.self,
-            onContext: batteryFetchContext,
-            predicate: NSPredicate.predicateFor30MinAgo,
-            key: "date",
-            ascending: false
-        )
-
-        return try await batteryFetchContext.perform {
-            guard let fetchedResults = results as? [OpenAPS_Battery] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return fetchedResults.map(\.objectID)
+        do {
+            try batteryController.performFetch()
+            updateBatteryFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform battery fetch: \(error)")
         }
     }
 
-    @MainActor private func updateBatteryArray(with objects: [OpenAPS_Battery]) {
+    @MainActor private func updateBatteryFromController() {
+        guard let objects = batteryController.fetchedObjects else { return }
         batteryFromPersistence = objects
     }
+
+    /// Called from the `pumpDisplayState` sink, `settingsDidChange` and `pumpSettingsDidChange`.
+    func setupBatteryArray() {
+        Task { @MainActor in
+            updateBatteryFromController()
+        }
+    }
 }

+ 24 - 81
Trio/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift

@@ -2,104 +2,47 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupCarbsArray() {
-        Task {
-            do {
-                let ids = try await self.fetchCarbs()
+    // MARK: - Carbs
 
-                // Prefetch into viewContext with one IN-query so the subsequent
-                // per-ID materialization avoids N+1 Z_PK selects.
-                if !ids.isEmpty {
-                    await viewContext.perform {
-                        let prefetchRequest = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
-                        prefetchRequest.predicate = NSPredicate(format: "SELF IN %@", ids)
-                        prefetchRequest.returnsObjectsAsFaults = false
-                        _ = try? self.viewContext.fetch(prefetchRequest)
-                    }
-                }
-
-                let carbObjects: [CarbEntryStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateCarbsArray(with: carbObjects)
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) Error fetching carb objects: \(error) in \(#file):\(#line)")
+    @MainActor func setupCarbsController() {
+        carbsControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateCarbsFromController()
             }
         }
-    }
-
-    private func fetchCarbs() async throws -> [NSManagedObjectID] {
-        let carbsFetchContext = CoreDataStack.shared.newTaskContext()
-        carbsFetchContext.name = "HomeStateModel.fetchCarbs"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: carbsFetchContext,
-            predicate: NSPredicate.carbsForChart,
-            key: "date",
-            ascending: false,
-            batchSize: 5
-        )
-
-        return try await carbsFetchContext.perform {
-            guard let fetchedResults = results as? [CarbEntryStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
 
-            return fetchedResults.map(\.objectID)
+        do {
+            try carbsController.performFetch()
+            updateCarbsFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform carbs fetch: \(error)")
         }
     }
 
-    @MainActor private func updateCarbsArray(with objects: [CarbEntryStored]) {
+    @MainActor private func updateCarbsFromController() {
+        guard let objects = carbsController.fetchedObjects else { return }
         carbsFromPersistence = objects
     }
 
-    func setupFPUsArray() {
-        Task {
-            do {
-                let ids = try await self.fetchFPUs()
+    // MARK: - FPUs
 
-                // Prefetch into viewContext with one IN-query so the subsequent
-                // per-ID materialization avoids N+1 Z_PK selects.
-                if !ids.isEmpty {
-                    await viewContext.perform {
-                        let prefetchRequest = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
-                        prefetchRequest.predicate = NSPredicate(format: "SELF IN %@", ids)
-                        prefetchRequest.returnsObjectsAsFaults = false
-                        _ = try? self.viewContext.fetch(prefetchRequest)
-                    }
-                }
-
-                let fpuObjects: [CarbEntryStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateFPUsArray(with: fpuObjects)
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) Error fetching FPU objects: \(error) in \(#file):\(#line)")
+    @MainActor func setupFPUController() {
+        fpuControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateFPUsFromController()
             }
         }
-    }
-
-    private func fetchFPUs() async throws -> [NSManagedObjectID] {
-        let fpuFetchContext = CoreDataStack.shared.newTaskContext()
-        fpuFetchContext.name = "HomeStateModel.fetchFPUs"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: CarbEntryStored.self,
-            onContext: fpuFetchContext,
-            predicate: NSPredicate.fpusForChart,
-            key: "date",
-            ascending: false
-        )
-
-        return try await fpuFetchContext.perform {
-            guard let fetchedResults = results as? [CarbEntryStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
 
-            return fetchedResults.map(\.objectID)
+        do {
+            try fpuController.performFetch()
+            updateFPUsFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform FPU fetch: \(error)")
         }
     }
 
-    @MainActor private func updateFPUsArray(with objects: [CarbEntryStored]) {
+    @MainActor private func updateFPUsFromController() {
+        guard let objects = fpuController.fetchedObjects else { return }
         fpusFromPersistence = objects
     }
 }

+ 46 - 92
Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift

@@ -2,111 +2,65 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func yAxisChartData(glucoseValues: [GlucoseStored], on context: NSManagedObjectContext) {
-        // Capture the forecast values from `preprocessedData` on the main thread
-        Task { @MainActor in
-            let forecastValues = self.preprocessedData.map { Decimal($0.forecastValue.value) }
+    /// Recomputes the main glucose chart Y axis bounds from the fetched glucose objects.
+    /// Runs on the main actor since the inputs are viewContext managed objects.
+    @MainActor func updateGlucoseChartYAxis(glucoseValues: [GlucoseStored]) {
+        let glucoseMapped = glucoseValues.map { Decimal($0.glucose) }
+        let forecastValues = preprocessedData.map { Decimal($0.forecastValue.value) }
 
-            // Perform the glucose processing on the background context
-            context.perform {
-                let glucoseMapped = glucoseValues.map { Decimal($0.glucose) }
-
-                // Calculate min and max values for glucose and forecast
-                let minGlucose = glucoseMapped.min()
-                let maxGlucose = glucoseMapped.max()
-                let minForecast = forecastValues.min()
-                let maxForecast = forecastValues.max()
-
-                // Ensure all values exist, otherwise set default values
-                guard let minGlucose = minGlucose, let maxGlucose = maxGlucose else {
-                    Task {
-                        await self.updateChartBounds(minValue: 39, maxValue: 200)
-                    }
-                    return
-                }
+        // Ensure all values exist, otherwise set default values
+        guard let minGlucose = glucoseMapped.min(), let maxGlucose = glucoseMapped.max() else {
+            minYAxisValue = 39
+            maxYAxisValue = 200
+            return
+        }
 
-                // Adjust max forecast to be no more than 50 over max glucose
-                let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 50, maxGlucose + 50)
-                let minOverall = min(minGlucose, minForecast ?? minGlucose)
-                let maxOverall = max(maxGlucose, adjustedMaxForecast)
+        let minForecast = forecastValues.min()
+        let maxForecast = forecastValues.max()
 
-                var maxYValue = Decimal(200)
-                if maxOverall > 200, maxOverall <= 225 {
-                    maxYValue = Decimal(250)
-                } else if maxOverall > 225, maxOverall <= 275 {
-                    maxYValue = Decimal(300)
-                } else if maxOverall > 275, maxOverall <= 325 {
-                    maxYValue = Decimal(350)
-                } else if maxOverall > 325 {
-                    maxYValue = Decimal(400)
-                }
+        // Adjust max forecast to be no more than 50 over max glucose
+        let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 50, maxGlucose + 50)
+        let minOverall = min(minGlucose, minForecast ?? minGlucose)
+        let maxOverall = max(maxGlucose, adjustedMaxForecast)
 
-                // Update the chart bounds on the main thread
-                Task {
-                    await self.updateChartBounds(minValue: minOverall, maxValue: maxYValue)
-                }
-            }
+        var maxYValue: Decimal = 200
+        if maxOverall > 200, maxOverall <= 225 {
+            maxYValue = 250
+        } else if maxOverall > 225, maxOverall <= 275 {
+            maxYValue = 300
+        } else if maxOverall > 275, maxOverall <= 325 {
+            maxYValue = 350
+        } else if maxOverall > 325 {
+            maxYValue = 400
         }
-    }
 
-    @MainActor private func updateChartBounds(minValue: Decimal, maxValue: Decimal) async {
-        minYAxisValue = minValue
-        maxYAxisValue = maxValue
+        minYAxisValue = minOverall
+        maxYAxisValue = maxYValue
     }
 
-    func yAxisChartDataCobChart(determinations: [[String: Any]], on context: NSManagedObjectContext) {
-        context.perform {
-            // Map the COB values from the dictionary results
-            let cobMapped = determinations.compactMap { entry in
-                // First cast to Int16, then convert to Decimal
-                if let cobValue = entry["cob"] as? Int16 {
-                    return Decimal(cobValue)
-                }
-                return nil
-            }
-            let maxCob = cobMapped.max()
+    /// Recomputes the COB chart Y axis bounds from the fetched determination objects.
+    @MainActor func yAxisChartDataCobChart(determinations: [OrefDetermination]) {
+        let cobMapped = determinations.map { Decimal($0.cob) }
 
-            // Ensure the result exists or set default values
-            if let maxCob = maxCob {
-                let calculatedMax = maxCob == 0 ? 20 : maxCob + 20
-                Task {
-                    await self.updateCobChartBounds(minValue: 0, maxValue: calculatedMax)
-                }
-            } else {
-                Task {
-                    await self.updateCobChartBounds(minValue: 0, maxValue: 20)
-                }
-            }
+        if let maxCob = cobMapped.max() {
+            minValueCobChart = 0
+            maxValueCobChart = maxCob == 0 ? 20 : maxCob + 20
+        } else {
+            minValueCobChart = 0
+            maxValueCobChart = 20
         }
     }
 
-    @MainActor private func updateCobChartBounds(minValue: Decimal, maxValue: Decimal) {
-        minValueCobChart = minValue
-        maxValueCobChart = maxValue
-    }
-
-    func yAxisChartDataIobChart(determinations: [[String: Any]], on context: NSManagedObjectContext) {
-        context.perform {
-            // Map the IOB values from the fetched dictionaries
-            let iobMapped = determinations.compactMap { ($0["iob"] as? NSDecimalNumber)?.decimalValue }
-            let minIob = iobMapped.min()
-            let maxIob = iobMapped.max()
+    /// Recomputes the IOB chart Y axis bounds from the fetched determination objects.
+    @MainActor func yAxisChartDataIobChart(determinations: [OrefDetermination]) {
+        let iobMapped = determinations.compactMap { $0.iob?.decimalValue }
 
-            // Ensure min and max IOB values exist, or set defaults
-            if let minIob = minIob, let maxIob = maxIob {
-                Task {
-                    await self.updateIobChartBounds(minValue: minIob, maxValue: maxIob)
-                }
-            } else {
-                Task {
-                    await self.updateIobChartBounds(minValue: 0, maxValue: 5)
-                }
-            }
+        if let minIob = iobMapped.min(), let maxIob = iobMapped.max() {
+            minValueIobChart = minIob
+            maxValueIobChart = maxIob
+        } else {
+            minValueIobChart = 0
+            maxValueIobChart = 5
         }
     }
-
-    @MainActor private func updateIobChartBounds(minValue: Decimal, maxValue: Decimal) async {
-        minValueIobChart = minValue
-        maxValueIobChart = maxValue
-    }
 }

+ 13 - 44
Trio/Sources/Modules/Home/HomeStateModel+Setup/CurrentTDDSetup.swift

@@ -2,54 +2,23 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupTDDArray() {
-        Task {
-            do {
-                // Get the NSManagedObjectIDs
-                let tddObjectIds = try await fetchTDDIDs()
-
-                // Get the NSManagedObjects and map them to TDD on the Main Thread
-                try await updateTDDArray(with: tddObjectIds, keyPath: \.fetchedTDDs)
-            } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch TDDs: \(error)")
+    @MainActor func setupTDDController() {
+        tddControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateTDDFromController()
             }
         }
-    }
 
-    @MainActor private func updateTDDArray(
-        with IDs: [NSManagedObjectID],
-        keyPath: ReferenceWritableKeyPath<Home.StateModel, [TDD]>
-    ) async throws {
-        let tddObjects: [TDD] = try await CoreDataStack.shared
-            .getNSManagedObject(with: IDs, context: viewContext)
-            .compactMap { managedObject in
-                // Safely extract date and total as optional
-                let timestamp = managedObject.value(forKey: "date") as? Date
-                let totalDailyDose = (managedObject.value(forKey: "total") as? NSNumber)?.decimalValue
-                return TDD(totalDailyDose: totalDailyDose, timestamp: timestamp)
-            }
-        self[keyPath: keyPath] = tddObjects
+        do {
+            try tddController.performFetch()
+            updateTDDFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform TDD fetch: \(error)")
+        }
     }
 
-    private func fetchTDDIDs() async throws -> [NSManagedObjectID] {
-        let tddFetchContext = CoreDataStack.shared.newTaskContext()
-        tddFetchContext.name = "HomeStateModel.fetchTDDIDs"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: TDDStored.self,
-            onContext: tddFetchContext,
-            predicate: NSPredicate.predicateForOneDayAgo,
-            key: "date",
-            ascending: false,
-            fetchLimit: 1,
-            propertiesToFetch: ["total", "date", "objectID"]
-        )
-
-        return await tddFetchContext.perform {
-            guard let fetchedResults = results as? [[String: Any]] else {
-                return []
-            }
-            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
-        }
+    @MainActor private func updateTDDFromController() {
+        guard let objects = tddController.fetchedObjects else { return }
+        fetchedTDDs = objects.map { TDD(totalDailyDose: $0.total?.decimalValue, timestamp: $0.date) }
     }
 }

+ 38 - 59
Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift

@@ -2,75 +2,54 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupDeterminationsArray() {
-        Task {
-            do {
-                // Get the NSManagedObjectIDs
-                async let enactedObjectIds = determinationStorage
-                    .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
-                async let enactedAndNonEnactedObjectIds = fetchCobAndIob()
-
-                let enactedIDs = try await enactedObjectIds
-                let enactedAndNonEnactedIds = try await enactedAndNonEnactedObjectIds
-
-                // Get the NSManagedObjects and return them on the Main Thread
-                try await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
-                try await updateDeterminationsArray(with: enactedAndNonEnactedIds, keyPath: \.enactedAndNonEnactedDeterminations)
-
-                await updateForecastData()
-            } catch let error as CoreDataError {
-                debug(.default, "Core Data error in setupDeterminationsArray: \(error)")
-            } catch {
-                debug(.default, "Unexpected error in setupDeterminationsArray: \(error)")
+    // MARK: - Enacted Determination
+
+    @MainActor func setupEnactedDeterminationController() {
+        enactedDeterminationControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                guard let self else { return }
+                self.updateEnactedDeterminationFromController()
+                await self.updateForecastData()
             }
         }
-    }
 
-    @MainActor private func updateDeterminationsArray(
-        with IDs: [NSManagedObjectID],
-        keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) async throws {
-        // Prefetch the determinations into viewContext with one IN-query so the
-        // subsequent per-ID materialization avoids N+1 faults.
-        if !IDs.isEmpty {
-            let prefetchRequest = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
-            prefetchRequest.predicate = NSPredicate(format: "SELF IN %@", IDs)
-            prefetchRequest.returnsObjectsAsFaults = false
-            _ = try? viewContext.fetch(prefetchRequest)
+        do {
+            try enactedDeterminationController.performFetch()
+            updateEnactedDeterminationFromController()
+            Task { @MainActor in
+                await self.updateForecastData()
+            }
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform enacted determination fetch: \(error)")
         }
-
-        // Fetch the objects off the main thread
-        let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
-            .getNSManagedObject(with: IDs, context: viewContext)
-
-        // Update the array on the main thread
-        self[keyPath: keyPath] = determinationObjects
     }
 
-    // Custom fetch to more efficiently filter only for cob and iob
-    private func fetchCobAndIob() async throws -> [NSManagedObjectID] {
-        let determinationFetchContext = CoreDataStack.shared.newTaskContext()
-        determinationFetchContext.name = "HomeStateModel.fetchCobAndIob"
+    @MainActor private func updateEnactedDeterminationFromController() {
+        guard let objects = enactedDeterminationController.fetchedObjects else { return }
+        determinationsFromPersistence = objects
+    }
 
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: determinationFetchContext,
-            predicate: NSPredicate.determinationsForCobIobCharts,
-            key: "deliverAt",
-            ascending: false,
-            batchSize: 50,
-            propertiesToFetch: ["cob", "iob", "deliverAt", "objectID"]
-        )
+    // MARK: - Determinations for COB/IOB Charts
 
-        return try await determinationFetchContext.perform {
-            guard let fetchedResults = results as? [[String: Any]] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
+    @MainActor func setupDeterminationController() {
+        determinationControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateDeterminationsFromController()
             }
+        }
 
-            // Update Chart Scales
-            self.yAxisChartDataCobChart(determinations: fetchedResults, on: determinationFetchContext)
-            self.yAxisChartDataIobChart(determinations: fetchedResults, on: determinationFetchContext)
-            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+        do {
+            try determinationController.performFetch()
+            updateDeterminationsFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform determination fetch: \(error)")
         }
     }
+
+    @MainActor private func updateDeterminationsFromController() {
+        guard let objects = determinationController.fetchedObjects else { return }
+        enactedAndNonEnactedDeterminations = objects
+        yAxisChartDataCobChart(determinations: objects)
+        yAxisChartDataIobChart(determinations: objects)
+    }
 }

+ 19 - 37
Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -2,50 +2,32 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupGlucoseArray() {
-        Task {
-            do {
-                let ids = try await self.fetchGlucose()
-                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateGlucoseArray(with: glucoseObjects)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error)"
-                )
+    @MainActor func setupGlucoseController() {
+        glucoseControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateGlucoseFromController()
             }
         }
-    }
-
-    private func fetchGlucose() async throws -> [NSManagedObjectID] {
-        let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
-        glucoseFetchContext.name = "HomeStateModel.fetchGlucose"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: glucoseFetchContext,
-            predicate: NSPredicate.glucose,
-            key: "date",
-            ascending: true,
-            batchSize: 50
-        )
-
-        return try await glucoseFetchContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
 
-            // Update Main Chart Y Axis Values
-            // Perform everything on "context" to be thread safe
-            self.yAxisChartData(glucoseValues: fetchedResults, on: glucoseFetchContext)
-
-            return fetchedResults.map(\.objectID)
+        do {
+            try glucoseController.performFetch()
+            updateGlucoseFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform glucose fetch: \(error)")
         }
     }
 
-    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+    @MainActor func updateGlucoseFromController() {
+        guard let objects = glucoseController.fetchedObjects else { return }
         glucoseFromPersistence = objects
         latestTwoGlucoseValues = Array(objects.suffix(2))
+        updateGlucoseChartYAxis(glucoseValues: objects)
+    }
+
+    /// Called from `MainChartView` on `.onChange(of: units)` to recompute the glucose-derived chart state.
+    func setupGlucoseArray() {
+        Task { @MainActor in
+            updateGlucoseFromController()
+        }
     }
 }

+ 28 - 63
Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift

@@ -2,87 +2,52 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    // Setup Overrides
-    func setupOverrides() {
-        Task {
-            do {
-                let ids = try await self.fetchOverrides()
-                let overrideObjects: [OverrideStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateOverrideArray(with: overrideObjects)
-            } catch let error as CoreDataError {
-                debug(.default, "Core Data error in setupOverrides: \(error)")
-            } catch {
-                debug(.default, "Unexpected error in setupOverrides: \(error)")
+    // MARK: - Overrides
+
+    @MainActor func setupOverrideController() {
+        overrideControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateOverridesFromController()
             }
         }
-    }
 
-    private func fetchOverrides() async throws -> [NSManagedObjectID] {
-        let overrideFetchContext = CoreDataStack.shared.newTaskContext()
-        overrideFetchContext.name = "HomeStateModel.fetchOverrides"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideStored.self,
-            onContext: overrideFetchContext,
-            predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
-            key: "date",
-            ascending: false
-        )
-
-        return try await overrideFetchContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return fetchedResults.map(\.objectID)
+        do {
+            try overrideController.performFetch()
+            updateOverridesFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform override fetch: \(error)")
         }
     }
 
-    @MainActor private func updateOverrideArray(with objects: [OverrideStored]) {
+    @MainActor private func updateOverridesFromController() {
+        guard let objects = overrideController.fetchedObjects else { return }
         overrides = objects
     }
 
-    // Setup expired Overrides
-    func setupOverrideRunStored() {
-        Task {
-            do {
-                let ids = try await self.fetchOverrideRunStored()
-                let overrideRunObjects: [OverrideRunStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateOverrideRunStoredArray(with: overrideRunObjects)
-            } catch let error as CoreDataError {
-                debug(.default, "Core Data error in setupOverrideRunStored: \(error)")
-            } catch {
-                debug(.default, "Unexpected error in setupOverrideRunStored: \(error)")
+    // MARK: - Override Runs
+
+    @MainActor func setupOverrideRunController() {
+        overrideRunControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateOverrideRunsFromController()
             }
         }
-    }
 
-    private func fetchOverrideRunStored() async throws -> [NSManagedObjectID] {
-        let overrideFetchContext = CoreDataStack.shared.newTaskContext()
-        overrideFetchContext.name = "HomeStateModel.fetchOverrideRunStored"
-
-        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideRunStored.self,
-            onContext: overrideFetchContext,
-            predicate: predicate,
-            key: "startDate",
-            ascending: false
-        )
-
-        return try await overrideFetchContext.perform {
-            guard let fetchedResults = results as? [OverrideRunStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return fetchedResults.map(\.objectID)
+        do {
+            try overrideRunController.performFetch()
+            updateOverrideRunsFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform override run fetch: \(error)")
         }
     }
 
-    @MainActor private func updateOverrideRunStoredArray(with objects: [OverrideRunStored]) {
+    @MainActor private func updateOverrideRunsFromController() {
+        guard let objects = overrideRunController.fetchedObjects else { return }
         overrideRunStored = objects
     }
 
+    // MARK: - Override Actions
+
     /// Cancels the running Override, creates an entry in the OverrideRunStored Core Data entity and posts a custom notification so that the AdjustmentsView gets updated
     @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
         do {

+ 34 - 88
Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -2,113 +2,59 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupInsulinArray() {
-        Task {
-            do {
-                let ids = try await self.fetchInsulin()
-
-                // Prefetch events and their bolus/tempBasal relationships into viewContext
-                // with one IN-query so the subsequent per-ID materialization avoids N+1 faults.
-                if !ids.isEmpty {
-                    await viewContext.perform {
-                        let prefetchRequest = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
-                        prefetchRequest.predicate = NSPredicate(format: "SELF IN %@", ids)
-                        prefetchRequest.relationshipKeyPathsForPrefetching = ["bolus", "tempBasal"]
-                        prefetchRequest.returnsObjectsAsFaults = false
-                        _ = try? self.viewContext.fetch(prefetchRequest)
-                    }
-                }
-
-                let insulinObjects: [PumpEventStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateInsulinArray(with: insulinObjects)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up insulin array: \(error)"
-                )
+    // MARK: - Insulin / Pump History
+
+    @MainActor func setupInsulinController() {
+        insulinControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                guard let self else { return }
+                self.updateInsulinFromController()
+                self.displayPumpStatusHighlightMessage()
+                self.displayPumpStatusBadge()
             }
         }
-    }
-
-    private func fetchInsulin() async throws -> [NSManagedObjectID] {
-        let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
-        pumpHistoryFetchContext.name = "HomeStateModel.fetchInsulin"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: NSPredicate.pumpHistoryLast24h,
-            key: "timestamp",
-            ascending: true,
-            batchSize: 30
-        )
 
-        return try await pumpHistoryFetchContext.perform {
-            guard let pumpEvents = results as? [PumpEventStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-
-            return pumpEvents.map(\.objectID)
+        do {
+            try insulinController.performFetch()
+            updateInsulinFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform insulin fetch: \(error)")
         }
     }
 
-    @MainActor private func updateInsulinArray(with insulinObjects: [PumpEventStored]) {
-        insulinFromPersistence = insulinObjects
+    @MainActor private func updateInsulinFromController() {
+        guard let objects = insulinController.fetchedObjects else { return }
+        insulinFromPersistence = objects
 
         manualTempBasal = apsManager.isManualTempBasal
-        tempBasals = insulinFromPersistence.filter { $0.tempBasal != nil }
-
-        suspendAndResumeEvents = insulinFromPersistence.filter {
+        tempBasals = objects.filter { $0.tempBasal != nil }
+        suspendAndResumeEvents = objects.filter {
             $0.type == EventType.pumpSuspend.rawValue || $0.type == EventType.pumpResume.rawValue
         }
     }
 
-    // Setup Last Bolus to display the bolus progress bar
-    // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
-    func setupLastBolus() {
-        Task {
-            do {
-                guard let id = try await self.fetchLastBolus() else { return }
-                await updateLastBolus(with: id)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error)"
-                )
-            }
-        }
-    }
-
-    func fetchLastBolus() async throws -> NSManagedObjectID? {
-        let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
-        pumpHistoryFetchContext.name = "HomeStateModel.fetchLastBolus"
+    // MARK: - Last Bolus
 
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: pumpHistoryFetchContext,
-            predicate: NSPredicate.lastPumpBolus,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1
-        )
+    //
+    // Drives the bolus progress bar. The predicate filters out external boluses so the progress bar
+    // does not display the amount of an external bolus added after a pump bolus.
 
-        return try await pumpHistoryFetchContext.perform {
-            guard let fetchedResults = results as? [PumpEventStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
+    @MainActor func setupLastBolusController() {
+        lastBolusControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateLastBolusFromController()
             }
-
-            return fetchedResults.map(\.objectID).first
         }
-    }
 
-    @MainActor private func updateLastBolus(with ID: NSManagedObjectID) {
         do {
-            lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
+            try lastBolusController.performFetch()
+            updateLastBolusFromController()
         } catch {
-            debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error)"
-            )
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform last bolus fetch: \(error)")
         }
     }
+
+    @MainActor private func updateLastBolusFromController() {
+        lastPumpBolus = lastBolusController.fetchedObjects?.first
+    }
 }

+ 28 - 64
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -2,88 +2,52 @@ import CoreData
 import Foundation
 
 extension Home.StateModel {
-    func setupTempTargetsStored() {
-        Task {
-            do {
-                let ids = try await self.fetchTempTargets()
-                let tempTargetObjects: [TempTargetStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateTempTargetsArray(with: tempTargetObjects)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up tempTargetStored: \(error)"
-                )
+    // MARK: - Temp Targets
+
+    @MainActor func setupTempTargetController() {
+        tempTargetControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateTempTargetsFromController()
             }
         }
-    }
 
-    private func fetchTempTargets() async throws -> [NSManagedObjectID] {
-        let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
-        tempTargetFetchContext.name = "HomeStateModel.fetchTempTargets"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: TempTargetStored.self,
-            onContext: tempTargetFetchContext,
-            predicate: NSPredicate.tempTargetsForMainChart,
-            key: "date",
-            ascending: false
-        )
-
-        return try await tempTargetFetchContext.perform {
-            guard let fetchedResults = results as? [TempTargetStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return fetchedResults.map(\.objectID)
+        do {
+            try tempTargetController.performFetch()
+            updateTempTargetsFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform temp target fetch: \(error)")
         }
     }
 
-    @MainActor private func updateTempTargetsArray(with objects: [TempTargetStored]) {
+    @MainActor private func updateTempTargetsFromController() {
+        guard let objects = tempTargetController.fetchedObjects else { return }
         tempTargetStored = objects
     }
 
-    // Setup expired TempTargets
-    func setupTempTargetsRunStored() {
-        Task {
-            do {
-                let ids = try await self.fetchTempTargetRunStored()
-                let tempTargetRunObjects: [TempTargetRunStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up temp targetsRunStored: \(error)"
-                )
+    // MARK: - Temp Target Runs
+
+    @MainActor func setupTempTargetRunController() {
+        tempTargetRunControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateTempTargetRunsFromController()
             }
         }
-    }
 
-    private func fetchTempTargetRunStored() async throws -> [NSManagedObjectID] {
-        let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
-        tempTargetFetchContext.name = "HomeStateModel.fetchTempTargetRunStored"
-
-        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: TempTargetRunStored.self,
-            onContext: tempTargetFetchContext,
-            predicate: predicate,
-            key: "startDate",
-            ascending: false
-        )
-
-        return try await tempTargetFetchContext.perform {
-            guard let fetchedResults = results as? [TempTargetRunStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-            return fetchedResults.map(\.objectID)
+        do {
+            try tempTargetRunController.performFetch()
+            updateTempTargetRunsFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform temp target run fetch: \(error)")
         }
     }
 
-    @MainActor private func updateTempTargetRunStoredArray(with objects: [TempTargetRunStored]) {
+    @MainActor private func updateTempTargetRunsFromController() {
+        guard let objects = tempTargetRunController.fetchedObjects else { return }
         tempTargetRunStored = objects
     }
 
+    // MARK: - Temp Target Actions
+
     @MainActor func cancelTempTarget(withID id: NSManagedObjectID) async {
         do {
             guard let profileToCancel = try viewContext.existingObject(with: id) as? TempTargetStored else { return }

+ 230 - 120
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -130,9 +130,218 @@ extension Home {
 
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
-        // Queue for handling Core Data change notifications
-        private let queue = DispatchQueue(label: "HomeStateModel.queue", qos: .userInitiated)
-        private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
+        // MARK: - NSFetchedResultsControllers
+
+        //
+        // Each Core Data backed array on this state model is driven by an NSFetchedResultsController
+        // bound to the viewContext. The controllers keep their `fetchedObjects` continuously in sync
+        // with the viewContext (which in turn is fed by the persistent history merge in CoreDataStack)
+        // and notify us via their delegate's `onContentChange` closure. This replaces the previous
+        // hand-rolled `changedObjectsOnManagedObjectContextDidSavePublisher` + re-fetch approach.
+
+        @ObservationIgnored let glucoseControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var glucoseController: NSFetchedResultsController<GlucoseStored> = {
+            let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: true)]
+            request.predicate = NSPredicate.glucose
+            request.fetchBatchSize = 50
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = glucoseControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let carbsControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var carbsController: NSFetchedResultsController<CarbEntryStored> = {
+            let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)]
+            request.predicate = NSPredicate.carbsForChart
+            request.fetchBatchSize = 5
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = carbsControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let fpuControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var fpuController: NSFetchedResultsController<CarbEntryStored> = {
+            let request = NSFetchRequest<CarbEntryStored>(entityName: "CarbEntryStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)]
+            request.predicate = NSPredicate.fpusForChart
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = fpuControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let enactedDeterminationControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var enactedDeterminationController: NSFetchedResultsController<OrefDetermination> =
+            {
+                let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
+                request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
+                request.predicate = NSPredicate.enactedDetermination
+                request.fetchLimit = 1
+                let controller = NSFetchedResultsController(
+                    fetchRequest: request,
+                    managedObjectContext: viewContext,
+                    sectionNameKeyPath: nil,
+                    cacheName: nil
+                )
+                controller.delegate = enactedDeterminationControllerDelegate
+                return controller
+            }()
+
+        @ObservationIgnored let determinationControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var determinationController: NSFetchedResultsController<OrefDetermination> = {
+            let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
+            request.predicate = NSPredicate.determinationsForCobIobCharts
+            request.fetchBatchSize = 50
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = determinationControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let insulinControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var insulinController: NSFetchedResultsController<PumpEventStored> = {
+            let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: true)]
+            request.predicate = NSPredicate.pumpHistoryLast24h
+            request.fetchBatchSize = 30
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = insulinControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let lastBolusControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var lastBolusController: NSFetchedResultsController<PumpEventStored> = {
+            let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: false)]
+            request.predicate = NSPredicate.lastPumpBolus
+            request.fetchLimit = 1
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = lastBolusControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let overrideControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var overrideController: NSFetchedResultsController<OverrideStored> = {
+            let request = NSFetchRequest<OverrideStored>(entityName: "OverrideStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \OverrideStored.date, ascending: false)]
+            request.predicate = NSPredicate.lastActiveOverride
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = overrideControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let overrideRunControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var overrideRunController: NSFetchedResultsController<OverrideRunStored> = {
+            let request = NSFetchRequest<OverrideRunStored>(entityName: "OverrideRunStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \OverrideRunStored.startDate, ascending: false)]
+            request.predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = overrideRunControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let tempTargetControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var tempTargetController: NSFetchedResultsController<TempTargetStored> = {
+            let request = NSFetchRequest<TempTargetStored>(entityName: "TempTargetStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \TempTargetStored.date, ascending: false)]
+            request.predicate = NSPredicate.tempTargetsForMainChart
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = tempTargetControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let tempTargetRunControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var tempTargetRunController: NSFetchedResultsController<TempTargetRunStored> = {
+            let request = NSFetchRequest<TempTargetRunStored>(entityName: "TempTargetRunStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \TempTargetRunStored.startDate, ascending: false)]
+            request.predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = tempTargetRunControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let batteryControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var batteryController: NSFetchedResultsController<OpenAPS_Battery> = {
+            let request = NSFetchRequest<OpenAPS_Battery>(entityName: "OpenAPS_Battery")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \OpenAPS_Battery.date, ascending: false)]
+            request.predicate = NSPredicate.predicateFor30MinAgo
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = batteryControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let tddControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var tddController: NSFetchedResultsController<TDDStored> = {
+            let request = NSFetchRequest<TDDStored>(entityName: "TDDStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \TDDStored.date, ascending: false)]
+            request.predicate = NSPredicate.predicateForOneDayAgo
+            request.fetchLimit = 1
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = tddControllerDelegate
+            return controller
+        }()
+
         private var subscriptions = Set<AnyCancellable>()
 
         typealias PumpEvent = PumpEventStored.EventType
@@ -142,14 +351,7 @@ extension Home {
         }
 
         override func subscribe() {
-            coreDataPublisher =
-                changedObjectsOnManagedObjectContextDidSavePublisher()
-                    .receive(on: queue)
-                    .share()
-                    .eraseToAnyPublisher()
-
             registerSubscribers()
-            registerHandlers()
 
             // Parallelize Setup functions
             setupHomeViewConcurrently()
@@ -163,33 +365,25 @@ extension Home {
                 await self.setupCGMSettings()
                 self.registerObservers()
 
+                // Set up the NSFetchedResultsControllers. These are bound to the viewContext,
+                // so `performFetch` and the initial population must run on the main actor.
+                await self.setupGlucoseController()
+                await self.setupCarbsController()
+                await self.setupFPUController()
+                await self.setupEnactedDeterminationController()
+                await self.setupDeterminationController()
+                await self.setupInsulinController()
+                await self.setupLastBolusController()
+                await self.setupOverrideController()
+                await self.setupOverrideRunController()
+                await self.setupTempTargetController()
+                await self.setupTempTargetRunController()
+                await self.setupBatteryController()
+                await self.setupTDDController()
+
                 // The rest can be initialized concurrently
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
-                        self.setupGlucoseArray()
-                    }
-                    group.addTask {
-                        self.setupCarbsArray()
-                    }
-                    group.addTask {
-                        self.setupFPUsArray()
-                    }
-                    group.addTask {
-                        self.setupDeterminationsArray()
-                    }
-                    group.addTask {
-                        self.setupTDDArray()
-                    }
-                    group.addTask {
-                        self.setupInsulinArray()
-                    }
-                    group.addTask {
-                        self.setupLastBolus()
-                    }
-                    group.addTask {
-                        self.setupBatteryArray()
-                    }
-                    group.addTask {
                         await self.setupBasalProfile()
                     }
                     group.addTask {
@@ -199,25 +393,12 @@ extension Home {
                         self.setupReservoir()
                     }
                     group.addTask {
-                        self.setupOverrides()
-                    }
-                    group.addTask {
-                        self.setupOverrideRunStored()
-                    }
-                    group.addTask {
-                        self.setupTempTargetsStored()
-                    }
-                    group.addTask {
-                        self.setupTempTargetsRunStored()
-                    }
-                    group.addTask {
                         self.iobService.updateIOB()
                     }
                 }
             }
         }
 
-        // 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() {
             iobService.iobPublisher
                 .receive(on: DispatchQueue.main)
@@ -226,77 +407,6 @@ extension Home {
                     self.currentIOB = self.iobService.currentIOB ?? 0
                 }
                 .store(in: &subscriptions)
-
-            glucoseStorage.updatePublisher
-                .receive(on: queue)
-                .sink { [weak self] _ in
-                    guard let self = self else { return }
-                    self.setupGlucoseArray()
-                }
-                .store(in: &subscriptions)
-
-            carbsStorage.updatePublisher
-                .receive(on: queue)
-                .sink { [weak self] _ in
-                    guard let self = self else { return }
-                    self.setupFPUsArray()
-                }
-                .store(in: &subscriptions)
-        }
-
-        private func registerHandlers() {
-            coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupDeterminationsArray()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("TDDStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupTDDArray()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupGlucoseArray()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupCarbsArray()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupInsulinArray()
-                self.setupLastBolus()
-                self.displayPumpStatusHighlightMessage()
-                self.displayPumpStatusBadge()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("OpenAPS_Battery").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupBatteryArray()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupOverrides()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("OverrideRunStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupOverrideRunStored()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupTempTargetsStored()
-            }.store(in: &subscriptions)
-
-            coreDataPublisher?.filteredByEntityName("TempTargetRunStored").sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupTempTargetsRunStored()
-            }.store(in: &subscriptions)
         }
 
         private func registerObservers() {
@@ -500,7 +610,7 @@ extension Home {
 
         /// Display the eventual status message provided by the manager of the pump
         /// Only display if state is warning or critical message else return nil
-        private func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
+        func displayPumpStatusHighlightMessage(_ didDeactivate: Bool = false) {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
                 if let statusHighlight = self.provider.deviceManager.pumpManager?.pumpStatusHighlight,
@@ -514,7 +624,7 @@ extension Home {
             }
         }
 
-        private func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
+        func displayPumpStatusBadge(_ didDeactivate: Bool = false) {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
                 if let statusBadge = self.provider.deviceManager.pumpManager?.pumpStatusBadge,