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

Merge branch 'core-data-sync-trio' into override-update

Robert 1 год назад
Родитель
Сommit
7851f66a4f
53 измененных файлов с 812 добавлено и 726 удалено
  1. 4 4
      FreeAPS.xcodeproj/project.pbxproj
  2. 0 31
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  3. 9 10
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  4. 6 1
      FreeAPS/Sources/Helpers/Color+Extensions.swift
  5. 22 21
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  6. 1 1
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  7. 8 7
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  8. 1 1
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  9. 9 8
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  10. 1 1
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  11. 102 103
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  12. 1 0
      FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift
  13. 2 2
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  14. 23 14
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  15. 1 1
      FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift
  16. 1 1
      FreeAPS/Sources/Modules/Bolus/View/PopupView.swift
  17. 10 10
      FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  18. 1 1
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift
  19. 2 2
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  20. 19 18
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  21. 3 1
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  22. 13 12
      FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift
  23. 1 1
      FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  24. 3 5
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  25. 8 3
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  26. 105 85
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  27. 2 2
      FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift
  28. 17 17
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  29. 9 10
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  30. 6 6
      FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift
  31. 7 7
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  32. 35 34
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  33. 9 8
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  34. 1 1
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  35. 5 4
      FreeAPS/Sources/Modules/ManualTempBasal/ManualTempBasalStateModel.swift
  36. 1 1
      FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift
  37. 61 66
      FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift
  38. 2 2
      FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift
  39. 1 1
      FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift
  40. 19 18
      FreeAPS/Sources/Modules/SMBSettings/SMBSettingsStateModel.swift
  41. 1 1
      FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  42. 5 4
      FreeAPS/Sources/Modules/Snooze/SnoozeStateModel.swift
  43. 1 1
      FreeAPS/Sources/Modules/Snooze/View/SnoozeRootView.swift
  44. 10 9
      FreeAPS/Sources/Modules/Stat/StatStateModel.swift
  45. 16 16
      FreeAPS/Sources/Modules/Stat/View/ChartsView.swift
  46. 46 46
      FreeAPS/Sources/Modules/Stat/View/StatRootView.swift
  47. 12 12
      FreeAPS/Sources/Modules/Stat/View/StatsView.swift
  48. 31 31
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  49. 16 15
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  50. 10 11
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  51. 108 57
      FreeAPS/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  52. 6 2
      FreeAPS/Sources/Views/TextFieldWithToolBar.swift
  53. 19 0
      Model/Helper/CustomNotification.swift

+ 4 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -4028,7 +4028,7 @@
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			name = Debug;
 		};
@@ -4065,7 +4065,7 @@
 				SWIFT_OBJC_BRIDGING_HEADER = "Model/Classes+Properties/FreeAPSWatch-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			name = Release;
 		};
@@ -4105,7 +4105,7 @@
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			name = Debug;
 		};
@@ -4145,7 +4145,7 @@
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
-				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+				WATCHOS_DEPLOYMENT_TARGET = 10;
 			};
 			name = Release;
 		};

+ 0 - 31
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -86,37 +86,6 @@ final class OpenAPS {
 
         // First save the current Determination to Core Data
         await attemptToSaveContext()
-
-        // After that check for changes in iob and cob and if there are any post a custom Notification
-        /// this is currently used to update Live Activity so that it stays up to date and not one loop cycle behind
-        await checkForCobIobUpdate(determination)
-    }
-
-    func checkForCobIobUpdate(_ determination: Determination) async {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: context,
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination,
-            key: "deliverAt",
-            ascending: false,
-            fetchLimit: 2
-        )
-
-        await context.perform {
-            guard let previousDeterminations = results as? [OrefDetermination] else {
-                return
-            }
-
-            // We need to get the second last Determination for this comparison because we have saved the current Determination already to Core Data
-            if let previousDetermination = previousDeterminations.dropFirst().first {
-                let iobChanged = previousDetermination.iob != self.decimalToNSDecimalNumber(determination.iob)
-                let cobChanged = previousDetermination.cob != Int16(Int(determination.cob ?? 0))
-
-                if iobChanged || cobChanged {
-                    Foundation.NotificationCenter.default.post(name: .didUpdateCobIob, object: nil)
-                }
-            }
-        }
     }
 
     func attemptToSaveContext() async {

+ 9 - 10
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -82,11 +82,11 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         in context: NSManagedObjectContext
     ) async -> (UUID, Forecast?, [ForecastValue]) {
-        var forecast: Forecast?
-        var forecastValues: [ForecastValue] = []
+        return await context.perform {
+            var forecast: Forecast?
+            var forecastValues: [ForecastValue] = []
 
-        do {
-            try await context.perform {
+            do {
                 // Fetch the forecast object
                 forecast = try context.existingObject(with: data.forecastID) as? Forecast
 
@@ -96,14 +96,13 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                         forecastValues.append(forecastValue)
                     }
                 }
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
+                )
             }
-        } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
-            )
+            return (data.id, forecast, forecastValues)
         }
-
-        return (data.id, forecast, forecastValues)
     }
 
     // Convert NSDecimalNumber to Decimal

+ 6 - 1
FreeAPS/Sources/Helpers/Color+Extensions.swift

@@ -8,7 +8,12 @@ extension Color {
 
     static let glucose = Color("glucose")
 
-    static let insulin = Color("Insulin")
+    static let insulin =
+        // workaround for 'No color named 'Insulin' found in asset catalog' error which is most likely a bug
+        Color(
+            UIColor(named: "Insulin") ??
+                UIColor(red: 0.118, green: 0.588, blue: 0.988, alpha: 1.0) // these are RGB of our insulin color
+        )
 
     // The loopAccent color is intended to be use as the app accent color.
     public static let loopAccent = Color("accent")

+ 22 - 21
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -1,28 +1,29 @@
 import Combine
+import Observation
 import SwiftUI
 
 extension AlgorithmAdvancedSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
-        @Injected() var nightscout: NightscoutManager!
-
-        @Published var units: GlucoseUnits = .mgdL
-
-        @Published var maxDailySafetyMultiplier: Decimal = 3
-        @Published var currentBasalSafetyMultiplier: Decimal = 4
-        @Published var useCustomPeakTime: Bool = false
-        @Published var insulinPeakTime: Decimal = 75
-        @Published var skipNeutralTemps: Bool = false
-        @Published var unsuspendIfNoTemp: Bool = false
-        @Published var suspendZerosIOB: Bool = false
-        @Published var min5mCarbimpact: Decimal = 8
-        @Published var autotuneISFAdjustmentFraction: Decimal = 1.0
-        @Published var remainingCarbsFraction: Decimal = 1.0
-        @Published var remainingCarbsCap: Decimal = 90
-        @Published var noisyCGMTargetMultiplier: Decimal = 1.3
-
-        @Published var insulinActionCurve: Decimal = 6
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
+        @ObservationIgnored @Injected() var nightscout: NightscoutManager!
+
+        var units: GlucoseUnits = .mgdL
+
+        var maxDailySafetyMultiplier: Decimal = 3
+        var currentBasalSafetyMultiplier: Decimal = 4
+        var useCustomPeakTime: Bool = false
+        var insulinPeakTime: Decimal = 75
+        var skipNeutralTemps: Bool = false
+        var unsuspendIfNoTemp: Bool = false
+        var suspendZerosIOB: Bool = false
+        var min5mCarbimpact: Decimal = 8
+        var autotuneISFAdjustmentFraction: Decimal = 1.0
+        var remainingCarbsFraction: Decimal = 1.0
+        var remainingCarbsCap: Decimal = 90
+        var noisyCGMTargetMultiplier: Decimal = 1.3
+
+        var insulinActionCurve: Decimal = 6
 
         var preferences: Preferences {
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension AlgorithmAdvancedSettings {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?

+ 8 - 7
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -1,15 +1,16 @@
+import Observation
 import SwiftUI
 
 extension AutosensSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
 
-        @Published var units: GlucoseUnits = .mgdL
+        var units: GlucoseUnits = .mgdL
 
-        @Published var autosensMax: Decimal = 1.2
-        @Published var autosensMin: Decimal = 0.7
-        @Published var rewindResetsAutosens: Bool = true
+        var autosensMax: Decimal = 1.2
+        var autosensMin: Decimal = 0.7
+        var rewindResetsAutosens: Bool = true
 
         var preferences: Preferences {
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension AutosensSettings {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?

+ 9 - 8
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -1,14 +1,15 @@
+import Observation
 import SwiftUI
 
 extension BasalProfileEditor {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() private var nightscout: NightscoutManager!
-
-        @Published var syncInProgress: Bool = false
-        @Published var initialItems: [Item] = []
-        @Published var items: [Item] = []
-        @Published var total: Decimal = 0.0
-        @Published var showAlert: Bool = false
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
+
+        var syncInProgress: Bool = false
+        var initialItems: [Item] = []
+        var items: [Item] = []
+        var total: Decimal = 0.0
+        var showAlert: Bool = false
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 

+ 1 - 1
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension BasalProfileEditor {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var editMode = EditMode.inactive
 
         @Environment(\.colorScheme) var colorScheme

+ 102 - 103
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -2,117 +2,116 @@ import Combine
 import CoreData
 import Foundation
 import LoopKit
+import Observation
 import SwiftUI
 import Swinject
 
 extension Bolus {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var unlockmanager: UnlockManager!
-        @Injected() var apsManager: APSManager!
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var pumpHistoryStorage: PumpHistoryStorage!
-        // added for bolus calculator
-        @Injected() var settings: SettingsManager!
-        @Injected() var nsManager: NightscoutManager!
-        @Injected() var carbsStorage: CarbsStorage!
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var determinationStorage: DeterminationStorage!
-
-        @Published var lowGlucose: Decimal = 70
-        @Published var highGlucose: Decimal = 180
-        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
-
-        @Published var predictions: Predictions?
-        @Published var amount: Decimal = 0
-        @Published var insulinRecommended: Decimal = 0
-        @Published var insulinRequired: Decimal = 0
-        @Published var units: GlucoseUnits = .mgdL
-        @Published var threshold: Decimal = 0
-        @Published var maxBolus: Decimal = 0
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var nsManager: NightscoutManager!
+        @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+
+        var lowGlucose: Decimal = 70
+        var highGlucose: Decimal = 180
+        var glucoseColorScheme: GlucoseColorScheme = .staticColor
+
+        var predictions: Predictions?
+        var amount: Decimal = 0
+        var insulinRecommended: Decimal = 0
+        var insulinRequired: Decimal = 0
+        var units: GlucoseUnits = .mgdL
+        var threshold: Decimal = 0
+        var maxBolus: Decimal = 0
         var maxExternal: Decimal { maxBolus * 3 }
-        @Published var errorString: Decimal = 0
-        @Published var evBG: Decimal = 0
-        @Published var insulin: Decimal = 0
-        @Published var isf: Decimal = 0
-        @Published var error: Bool = false
-        @Published var minGuardBG: Decimal = 0
-        @Published var minDelta: Decimal = 0
-        @Published var expectedDelta: Decimal = 0
-        @Published var minPredBG: Decimal = 0
-        @Published var waitForSuggestion: Bool = false
-        @Published var carbRatio: Decimal = 0
-
-        @Published var addButtonPressed: Bool = false
+        var errorString: Decimal = 0
+        var evBG: Decimal = 0
+        var insulin: Decimal = 0
+        var isf: Decimal = 0
+        var error: Bool = false
+        var minGuardBG: Decimal = 0
+        var minDelta: Decimal = 0
+        var expectedDelta: Decimal = 0
+        var minPredBG: Decimal = 0
+        var waitForSuggestion: Bool = false
+        var carbRatio: Decimal = 0
+
+        var addButtonPressed: Bool = false
 
         var waitForSuggestionInitial: Bool = false
 
-        // added for bolus calculator
-        @Published var target: Decimal = 0
-        @Published var cob: Int16 = 0
-        @Published var iob: Decimal = 0
-
-        @Published var currentBG: Decimal = 0
-        @Published var fifteenMinInsulin: Decimal = 0
-        @Published var deltaBG: Decimal = 0
-        @Published var targetDifferenceInsulin: Decimal = 0
-        @Published var targetDifference: Decimal = 0
-        @Published var wholeCob: Decimal = 0
-        @Published var wholeCobInsulin: Decimal = 0
-        @Published var iobInsulinReduction: Decimal = 0
-        @Published var wholeCalc: Decimal = 0
-        @Published var insulinCalculated: Decimal = 0
-        @Published var fraction: Decimal = 0
-        @Published var basal: Decimal = 0
-        @Published var fattyMeals: Bool = false
-        @Published var fattyMealFactor: Decimal = 0
-        @Published var useFattyMealCorrectionFactor: Bool = false
-        @Published var displayPresets: Bool = true
-
-        @Published var currentBasal: Decimal = 0
-        @Published var currentCarbRatio: Decimal = 0
-        @Published var currentBGTarget: Decimal = 0
-        @Published var currentISF: Decimal = 0
-
-        @Published var sweetMeals: Bool = false
-        @Published var sweetMealFactor: Decimal = 0
-        @Published var useSuperBolus: Bool = false
-        @Published var superBolusInsulin: Decimal = 0
-
-        @Published var meal: [CarbsEntry]?
-        @Published var carbs: Decimal = 0
-        @Published var fat: Decimal = 0
-        @Published var protein: Decimal = 0
-        @Published var note: String = ""
-
-        @Published var date = Date()
-
-        @Published var carbsRequired: Decimal?
-        @Published var useFPUconversion: Bool = false
-        @Published var dish: String = ""
-        @Published var selection: MealPresetStored?
-        @Published var summation: [String] = []
-        @Published var maxCarbs: Decimal = 0
-        @Published var maxFat: Decimal = 0
-        @Published var maxProtein: Decimal = 0
-
-        @Published var id_: String = ""
-        @Published var summary: String = ""
-
-        @Published var externalInsulin: Bool = false
-        @Published var showInfo: Bool = false
-        @Published var glucoseFromPersistence: [GlucoseStored] = []
-        @Published var determination: [OrefDetermination] = []
-        @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
-        @Published var predictionsForChart: Predictions?
-        @Published var simulatedDetermination: Determination?
-        @Published var determinationObjectIDs: [NSManagedObjectID] = []
-
-        @Published var minForecast: [Int] = []
-        @Published var maxForecast: [Int] = []
-        @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
-        @Published var forecastDisplayType: ForecastDisplayType = .cone
-        @Published var isSmoothingEnabled: Bool = false
-        @Published var stops: [Gradient.Stop] = []
+        var target: Decimal = 0
+        var cob: Int16 = 0
+        var iob: Decimal = 0
+
+        var currentBG: Decimal = 0
+        var fifteenMinInsulin: Decimal = 0
+        var deltaBG: Decimal = 0
+        var targetDifferenceInsulin: Decimal = 0
+        var targetDifference: Decimal = 0
+        var wholeCob: Decimal = 0
+        var wholeCobInsulin: Decimal = 0
+        var iobInsulinReduction: Decimal = 0
+        var wholeCalc: Decimal = 0
+        var insulinCalculated: Decimal = 0
+        var fraction: Decimal = 0
+        var basal: Decimal = 0
+        var fattyMeals: Bool = false
+        var fattyMealFactor: Decimal = 0
+        var useFattyMealCorrectionFactor: Bool = false
+        var displayPresets: Bool = true
+
+        var currentBasal: Decimal = 0
+        var currentCarbRatio: Decimal = 0
+        var currentBGTarget: Decimal = 0
+        var currentISF: Decimal = 0
+
+        var sweetMeals: Bool = false
+        var sweetMealFactor: Decimal = 0
+        var useSuperBolus: Bool = false
+        var superBolusInsulin: Decimal = 0
+
+        var meal: [CarbsEntry]?
+        var carbs: Decimal = 0
+        var fat: Decimal = 0
+        var protein: Decimal = 0
+        var note: String = ""
+
+        var date = Date()
+
+        var carbsRequired: Decimal?
+        var useFPUconversion: Bool = false
+        var dish: String = ""
+        var selection: MealPresetStored?
+        var summation: [String] = []
+        var maxCarbs: Decimal = 0
+        var maxFat: Decimal = 0
+        var maxProtein: Decimal = 0
+
+        var id_: String = ""
+        var summary: String = ""
+
+        var externalInsulin: Bool = false
+        var showInfo: Bool = false
+        var glucoseFromPersistence: [GlucoseStored] = []
+        var determination: [OrefDetermination] = []
+        var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
+        var predictionsForChart: Predictions?
+        var simulatedDetermination: Determination?
+        var determinationObjectIDs: [NSManagedObjectID] = []
+
+        var minForecast: [Int] = []
+        var maxForecast: [Int] = []
+        var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        var forecastDisplayType: ForecastDisplayType = .cone
+        var isSmoothingEnabled: Bool = false
+        var stops: [Gradient.Stop] = []
 
         let now = Date.now
 

+ 1 - 0
FreeAPS/Sources/Modules/Bolus/View/AddMealPresetView.swift

@@ -1,5 +1,6 @@
 import CoreData
 import Foundation
+import Observation
 import SwiftUI
 
 struct AddMealPresetView: View {

+ 2 - 2
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -17,7 +17,7 @@ extension Bolus {
 
         let resolver: Resolver
 
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
         @State private var showPresetSheet = false
         @State private var autofocus: Bool = true
@@ -175,7 +175,7 @@ extension Bolus {
                 VStack {
                     List {
                         Section {
-                            ForecastChart(state: state, units: $state.units)
+                            ForecastChart(state: state)
                                 .padding(.vertical)
                         }.listRowBackground(Color.chart)
 

+ 23 - 14
FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift

@@ -4,9 +4,8 @@ import Foundation
 import SwiftUI
 
 struct ForecastChart: View {
-    @StateObject var state: Bolus.StateModel
+    var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
-    @Binding var units: GlucoseUnits
 
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
 
@@ -23,7 +22,7 @@ struct ForecastChart: View {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
 
-        if units == .mmolL {
+        if state.units == .mmolL {
             formatter.maximumFractionDigits = 1
             formatter.minimumFractionDigits = 1
             formatter.roundingMode = .halfUp
@@ -78,11 +77,21 @@ struct ForecastChart: View {
                 if let simulatedDetermination = state.simulatedDetermination, let eventualBG = simulatedDetermination.eventualBG {
                     HStack {
                         Text(
-                            (units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL) + units.rawValue
+                            state.units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
                         )
+                        .font(.footnote)
+                        .foregroundStyle(.primary)
+                        Text("\(state.units.rawValue)")
+                            .font(.footnote)
+                            .foregroundStyle(.secondary)
                     }
                 } else {
                     Text("---")
+                        .font(.footnote)
+                        .foregroundStyle(.primary)
+                    Text("\(state.units.rawValue)")
+                        .font(.footnote)
+                        .foregroundStyle(.secondary)
                 }
             }
             .font(.footnote)
@@ -109,7 +118,7 @@ struct ForecastChart: View {
         .chartXAxis { forecastChartXAxis }
         .chartXScale(domain: startMarker ... endMarker)
         .chartYAxis { forecastChartYAxis }
-        .chartYScale(domain: units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
+        .chartYScale(domain: state.units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
         .backport.chartForegroundStyleScale(state: state)
     }
 
@@ -118,8 +127,8 @@ struct ForecastChart: View {
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
 
             // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
-            let lowGlucose = units == .mgdL ? state.lowGlucose : state.lowGlucose.asMgdL
-            let highGlucose = units == .mgdL ? state.highGlucose : state.highGlucose.asMgdL
+            let lowGlucose = state.units == .mgdL ? state.lowGlucose : state.lowGlucose.asMgdL
+            let highGlucose = state.units == .mgdL ? state.highGlucose : state.highGlucose.asMgdL
             let targetGlucose = (state.determination.first?.currentTarget ?? state.currentBGTarget as NSDecimalNumber) as Decimal
 
             let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
@@ -168,17 +177,17 @@ struct ForecastChart: View {
 
                 // if distance between respective min and max is 0, provide a default range
                 if yMinMaxDelta == 0 {
-                    let yMinValue = units == .mgdL ? Decimal(state.minForecast[index] - 1) :
+                    let yMinValue = state.units == .mgdL ? Decimal(state.minForecast[index] - 1) :
                         Decimal(state.minForecast[index] - 1)
                         .asMmolL
-                    let yMaxValue = units == .mgdL ? Decimal(state.minForecast[index] + 1) :
+                    let yMaxValue = state.units == .mgdL ? Decimal(state.minForecast[index] + 1) :
                         Decimal(state.minForecast[index] + 1)
                         .asMmolL
 
                     AreaMark(
                         x: .value("Time", xValue <= endMarker ? xValue : endMarker),
-                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
-                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                        yStart: .value("Min Value", state.units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", state.units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
                     )
                     .foregroundStyle(Color.blue.opacity(0.5))
                     .interpolationMethod(.catmullRom)
@@ -189,8 +198,8 @@ struct ForecastChart: View {
 
                     AreaMark(
                         x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
-                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
-                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                        yStart: .value("Min Value", state.units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", state.units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
                     )
                     .foregroundStyle(Color.blue.opacity(0.5))
                     .interpolationMethod(.catmullRom)
@@ -215,7 +224,7 @@ struct ForecastChart: View {
                 ForEach(values.indices, id: \.self) { index in
                     LineMark(
                         x: .value("Time", timeForIndex(Int32(index))),
-                        y: .value("Value", units == .mgdL ? Decimal(values[index]) : Decimal(values[index]).asMmolL)
+                        y: .value("Value", state.units == .mgdL ? Decimal(values[index]) : Decimal(values[index]).asMmolL)
                     )
                     .foregroundStyle(by: .value("Prediction Type", name))
                 }

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/MealPresetView.swift

@@ -3,7 +3,7 @@ import Foundation
 import SwiftUI
 
 struct MealPresetView: View {
-    @StateObject var state: Bolus.StateModel
+    @Bindable var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.dismiss) var dismiss
     @Environment(\.managedObjectContext) var moc

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -1,7 +1,7 @@
 import SwiftUI
 
 struct PopupView: View {
-    @StateObject var state: Bolus.StateModel
+    var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
 
     private var fractionDigits: Int {

+ 10 - 10
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -1,21 +1,21 @@
+import Observation
 import SwiftDate
 import SwiftUI
 
 extension Calibrations {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var calibrationService: CalibrationService!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var calibrationService: CalibrationService!
 
-        @Published var slope: Double = 1
-        @Published var intercept: Double = 1
-        @Published var newCalibration: Decimal = 0
-        @Published var calibrations: [Calibration] = []
-        @Published var calibrate: (Int) -> Double = { Double($0) }
-        @Published var items: [Item] = []
+        var slope: Double = 1
+        var intercept: Double = 1
+        var newCalibration: Decimal = 0
+        var calibrations: [Calibration] = []
+        var calibrate: (Int) -> Double = { Double($0) }
+        var items: [Item] = []
 
         var units: GlucoseUnits = .mgdL
 
-        // TODO: - test if we need to use the viewContext here
         private let context = CoreDataStack.shared.newTaskContext()
 
         override func subscribe() {

+ 1 - 1
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift

@@ -1,7 +1,7 @@
 import SwiftUI
 
 struct CalibrationsChart: View {
-    @EnvironmentObject var state: Calibrations.StateModel
+    var state: Calibrations.StateModel
 
     private var dateFormatter: DateFormatter {
         let formatter = DateFormatter()

+ 2 - 2
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension Calibrations {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
@@ -101,7 +101,7 @@ extension Calibrations {
 
                     if state.calibrations.isNotEmpty {
                         Section(header: Text("Chart")) {
-                            CalibrationsChart().environmentObject(state)
+                            CalibrationsChart(state: state)
                                 .frame(minHeight: geo.size.width)
                         }.listRowBackground(Color.chart)
                     }

+ 19 - 18
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -1,30 +1,31 @@
 import CoreData
 import HealthKit
+import Observation
 import SwiftUI
 
 extension DataTable {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var apsManager: APSManager!
-        @Injected() var unlockmanager: UnlockManager!
-        @Injected() private var storage: FileStorage!
-        @Injected() var pumpHistoryStorage: PumpHistoryStorage!
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var healthKitManager: HealthKitManager!
-        @Injected() var carbsStorage: CarbsStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
+        @ObservationIgnored @Injected() private var storage: FileStorage!
+        @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var healthKitManager: HealthKitManager!
+        @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
 
-        @Published var mode: Mode = .treatments
-        @Published var treatments: [Treatment] = []
-        @Published var glucose: [Glucose] = []
-        @Published var meals: [Treatment] = []
-        @Published var manualGlucose: Decimal = 0
-        @Published var maxBolus: Decimal = 0
-        @Published var waitForSuggestion: Bool = false
+        var mode: Mode = .treatments
+        var treatments: [Treatment] = []
+        var glucose: [Glucose] = []
+        var meals: [Treatment] = []
+        var manualGlucose: Decimal = 0
+        var maxBolus: Decimal = 0
+        var waitForSuggestion: Bool = false
 
-        @Published var insulinEntryDeleted: Bool = false
-        @Published var carbEntryDeleted: Bool = false
+        var insulinEntryDeleted: Bool = false
+        var carbEntryDeleted: Bool = false
 
         var units: GlucoseUnits = .mgdL
 

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

@@ -5,7 +5,9 @@ import Swinject
 extension DataTable {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+
+        @State var state = StateModel()
+
         @State private var isRemoveHistoryItemAlertPresented: Bool = false
         @State private var alertTitle: String = ""
         @State private var alertMessage: String = ""

+ 13 - 12
FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift

@@ -1,19 +1,20 @@
+import Observation
 import SwiftUI
 
 extension DynamicSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
 
-        @Published var useNewFormula: Bool = false
-        @Published var enableDynamicCR: Bool = false
-        @Published var sigmoid: Bool = false
-        @Published var adjustmentFactor: Decimal = 0.8
-        @Published var adjustmentFactorSigmoid: Decimal = 0.5
-        @Published var weightPercentage: Decimal = 0.65
-        @Published var tddAdjBasal: Bool = false
-        @Published var threshold_setting: Decimal = 60
-        @Published var units: GlucoseUnits = .mgdL
+        var useNewFormula: Bool = false
+        var enableDynamicCR: Bool = false
+        var sigmoid: Bool = false
+        var adjustmentFactor: Decimal = 0.8
+        var adjustmentFactorSigmoid: Decimal = 0.5
+        var weightPercentage: Decimal = 0.65
+        var tddAdjBasal: Bool = false
+        var threshold_setting: Decimal = 60
+        var units: GlucoseUnits = .mgdL
 
         var preferences: Preferences {
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension DynamicSettings {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?

+ 3 - 5
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift

@@ -17,9 +17,7 @@ extension Home.StateModel {
                 let maxForecast = forecastValues.max()
 
                 // Ensure all values exist, otherwise set default values
-                guard let minGlucose = minGlucose, let maxGlucose = maxGlucose,
-                      let minForecast = minForecast, let maxForecast = maxForecast
-                else {
+                guard let minGlucose = minGlucose, let maxGlucose = maxGlucose else {
                     Task {
                         await self.updateChartBounds(minValue: 39, maxValue: 300)
                     }
@@ -27,8 +25,8 @@ extension Home.StateModel {
                 }
 
                 // Adjust max forecast to be no more than 100 over max glucose
-                let adjustedMaxForecast = min(maxForecast, maxGlucose + 100)
-                let minOverall = min(minGlucose, minForecast)
+                let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 100, maxGlucose + 100)
+                let minOverall = min(minGlucose, minForecast ?? minGlucose)
                 let maxOverall = max(maxGlucose, adjustedMaxForecast)
 
                 // Update the chart bounds on the main thread

+ 8 - 3
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -80,19 +80,24 @@ extension Home.StateModel {
             return
         }
 
+        // Update minCount on the Main Thread
         minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
-        guard minCount > 0 else { return }
+
+        // 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
         let (minResult, maxResult) = await Task.detached {
-            let minForecast = (0 ..< self.minCount).map { index in
+            let minForecast = (0 ..< localMinCount).map { index in
                 localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
             }
 
-            let maxForecast = (0 ..< self.minCount).map { index in
+            let maxForecast = (0 ..< localMinCount).map { index in
                 localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
             }
 

+ 105 - 85
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -2,103 +2,107 @@ import Combine
 import CoreData
 import Foundation
 import LoopKitUI
+import Observation
 import SwiftDate
 import SwiftUI
 
 extension Home {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var apsManager: APSManager!
-        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
-        @Injected() var nightscoutManager: NightscoutManager!
-        @Injected() var determinationStorage: DeterminationStorage!
-        @Injected() var glucoseStorage: GlucoseStorage!
-        @Injected() var tempTargetStorage: TempTargetsStorage!
-        @Injected() var carbsStorage: CarbsStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
+        @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
+        @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
+        @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
-        @Published var manualGlucose: [BloodGlucose] = []
-        @Published var uploadStats = false
-        @Published var recentGlucose: BloodGlucose?
-        @Published var maxBasal: Decimal = 2
-        @Published var autotunedBasalProfile: [BasalProfileEntry] = []
-        @Published var basalProfile: [BasalProfileEntry] = []
-        @Published var timerDate = Date()
-        @Published var closedLoop = false
-        @Published var pumpSuspended = false
-        @Published var isLooping = false
-        @Published var statusTitle = ""
-        @Published var lastLoopDate: Date = .distantPast
-        @Published var battery: Battery?
-        @Published var reservoir: Decimal?
-        @Published var pumpName = ""
-        @Published var pumpExpiresAtDate: Date?
-        @Published var setupPump = false
-        @Published var errorMessage: String? = nil
-        @Published var errorDate: Date? = nil
-        @Published var bolusProgress: Decimal?
-        @Published var eventualBG: Int?
-        @Published var allowManualTemp = false
-        @Published var units: GlucoseUnits = .mgdL
-        @Published var pumpDisplayState: PumpDisplayState?
-        @Published var alarm: GlucoseAlarm?
-        @Published var manualTempBasal = false
-        @Published var isSmoothingEnabled = false
-        @Published var maxValue: Decimal = 1.2
-        @Published var lowGlucose: Decimal = 70
-        @Published var highGlucose: Decimal = 180
-        @Published var currentGlucoseTarget: Decimal = 100
-        @Published var overrideUnit: Bool = false
-        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
-        @Published var displayXgridLines: Bool = false
-        @Published var displayYgridLines: Bool = false
-        @Published var thresholdLines: Bool = false
-        @Published var timeZone: TimeZone?
-        @Published var hours: Int16 = 6
-        @Published var totalBolus: Decimal = 0
-        @Published var isStatusPopupPresented: Bool = false
-        @Published var isLegendPresented: Bool = false
-        @Published var legendSheetDetent = PresentationDetent.large
-        @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
-        @Published var roundedTotalBolus: String = ""
-        @Published var selectedTab: Int = 0
-        @Published var waitForSuggestion: Bool = false
-        @Published var glucoseFromPersistence: [GlucoseStored] = []
-        @Published var latestTwoGlucoseValues: [GlucoseStored] = []
-        @Published var carbsFromPersistence: [CarbEntryStored] = []
-        @Published var fpusFromPersistence: [CarbEntryStored] = []
-        @Published var determinationsFromPersistence: [OrefDetermination] = []
-        @Published var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
-        @Published var insulinFromPersistence: [PumpEventStored] = []
-        @Published var tempBasals: [PumpEventStored] = []
-        @Published var suspensions: [PumpEventStored] = []
-        @Published var batteryFromPersistence: [OpenAPS_Battery] = []
-        @Published var lastPumpBolus: PumpEventStored?
-        @Published var overrides: [OverrideStored] = []
-        @Published var overrideRunStored: [OverrideRunStored] = []
-        @Published var tempTargetStored: [TempTargetStored] = []
-        @Published var tempTargetRunStored: [TempTargetRunStored] = []
-        @Published var isOverrideCancelled: Bool = false
-        @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
-        @Published var pumpStatusHighlightMessage: String? = nil
-        @Published var cgmAvailable: Bool = false
-        @Published var showCarbsRequiredBadge: Bool = true
+        var manualGlucose: [BloodGlucose] = []
+        var announcement: [Announcement] = []
+        var uploadStats = false
+        var recentGlucose: BloodGlucose?
+        var maxBasal: Decimal = 2
+        var autotunedBasalProfile: [BasalProfileEntry] = []
+        var basalProfile: [BasalProfileEntry] = []
+        var tempTargets: [TempTarget] = []
+        var timerDate = Date()
+        var closedLoop = false
+        var pumpSuspended = false
+        var isLooping = false
+        var statusTitle = ""
+        var lastLoopDate: Date = .distantPast
+        var battery: Battery?
+        var reservoir: Decimal?
+        var pumpName = ""
+        var pumpExpiresAtDate: Date?
+        var tempTarget: TempTarget?
+        var setupPump = false
+        var errorMessage: String?
+        var errorDate: Date?
+        var bolusProgress: Decimal?
+        var eventualBG: Int?
+        var allowManualTemp = false
+        var units: GlucoseUnits = .mgdL
+        var pumpDisplayState: PumpDisplayState?
+        var alarm: GlucoseAlarm?
+        var manualTempBasal = false
+        var isSmoothingEnabled = false
+        var maxValue: Decimal = 1.2
+        var lowGlucose: Decimal = 70
+        var highGlucose: Decimal = 180
+        var currentGlucoseTarget: Decimal = 100
+        var glucoseColorScheme: GlucoseColorScheme = .staticColor
+        var overrideUnit: Bool = false
+        var displayXgridLines: Bool = false
+        var displayYgridLines: Bool = false
+        var thresholdLines: Bool = false
+        var timeZone: TimeZone?
+        var hours: Int16 = 6
+        var totalBolus: Decimal = 0
+        var isStatusPopupPresented: Bool = false
+        var isLegendPresented: Bool = false
+        var legendSheetDetent = PresentationDetent.large
+        var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
+        var roundedTotalBolus: String = ""
+        var selectedTab: Int = 0
+        var waitForSuggestion: Bool = false
+        var glucoseFromPersistence: [GlucoseStored] = []
+        var latestTwoGlucoseValues: [GlucoseStored] = []
+        var carbsFromPersistence: [CarbEntryStored] = []
+        var fpusFromPersistence: [CarbEntryStored] = []
+        var determinationsFromPersistence: [OrefDetermination] = []
+        var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
+        var insulinFromPersistence: [PumpEventStored] = []
+        var tempBasals: [PumpEventStored] = []
+        var suspensions: [PumpEventStored] = []
+        var batteryFromPersistence: [OpenAPS_Battery] = []
+        var lastPumpBolus: PumpEventStored?
+        var overrides: [OverrideStored] = []
+        var overrideRunStored: [OverrideRunStored] = []
+        var isOverrideCancelled: Bool = false
+        var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
+        var pumpStatusHighlightMessage: String?
+        var cgmAvailable: Bool = false
+        var showCarbsRequiredBadge: Bool = true
+        var tempTargetStored: [TempTargetStored] = []
+        var tempTargetRunStored: [TempTargetRunStored] = []
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         @Published var currentBGTarget: Decimal = 0
 
-        @Published var minForecast: [Int] = []
-        @Published var maxForecast: [Int] = []
-        @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
-        @Published var forecastDisplayType: ForecastDisplayType = .cone
+        var minForecast: [Int] = []
+        var maxForecast: [Int] = []
+        var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
+        var forecastDisplayType: ForecastDisplayType = .cone
 
-        @Published var minYAxisValue: Decimal = 39
-        @Published var maxYAxisValue: Decimal = 300
+        var minYAxisValue: Decimal = 39
+        var maxYAxisValue: Decimal = 300
 
-        @Published var minValueCobChart: Decimal = 0
-        @Published var maxValueCobChart: Decimal = 20
+        var minValueCobChart: Decimal = 0
+        var maxValueCobChart: Decimal = 20
 
-        @Published var minValueIobChart: Decimal = 0
-        @Published var maxValueIobChart: Decimal = 5
+        var minValueIobChart: Decimal = 0
+        var maxValueIobChart: Decimal = 5
 
         let taskContext = CoreDataStack.shared.newTaskContext()
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
@@ -450,6 +454,22 @@ extension Home {
             }
         }
 
+        @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
+            do {
+                let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
+                profileToCancel?.enabled = false
+
+                await saveToOverrideRunStored(withID: id)
+
+                guard viewContext.hasChanges else { return }
+                try viewContext.save()
+
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
+            }
+        }
+
         func calculateTINS() -> String {
             let startTime = calculateStartTime(hours: Int(hours))
 

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

@@ -25,11 +25,11 @@ extension MainChartView {
                 drawTempBasals(dummy: false)
                 drawBasalProfile()
                 drawSuspensions()
-            }.onChange(of: state.tempBasals) { _ in
+            }.onChange(of: state.tempBasals) {
                 calculateBasals()
                 calculateTempBasalsInBackground()
             }
-            .onChange(of: state.maxBasal) { _ in
+            .onChange(of: state.maxBasal) {
                 calculateBasals()
             }
             .frame(minHeight: geo.size.height * 0.05)

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

@@ -7,18 +7,18 @@ let calendar = Calendar.current
 
 struct MainChartView: View {
     var geo: GeometryProxy
-    @Binding var units: GlucoseUnits
-    @Binding var hours: Int
-    @Binding var highGlucose: Decimal
-    @Binding var lowGlucose: Decimal
-    @Binding var currentGlucoseTarget: Decimal
-    @Binding var screenHours: Int16
-    @Binding var glucoseColorScheme: GlucoseColorScheme
-    @Binding var displayXgridLines: Bool
-    @Binding var displayYgridLines: Bool
-    @Binding var thresholdLines: Bool
-
-    @StateObject var state: Home.StateModel
+    var units: GlucoseUnits
+    var hours: Int
+    var tempTargets: [TempTarget]
+    var highGlucose: Decimal
+    var lowGlucose: Decimal
+    var currentGlucoseTarget: Decimal
+    var glucoseColorScheme: GlucoseColorScheme
+    var screenHours: Int16
+    var displayXgridLines: Bool
+    var displayYgridLines: Bool
+    var thresholdLines: Bool
+    var state: Home.StateModel
 
     @State var basalProfiles: [BasalProfile] = []
     @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
@@ -96,17 +96,17 @@ struct MainChartView: View {
                                 iobChart
                             }
 
-                        }.onChange(of: screenHours) { _ in
+                        }.onChange(of: screenHours) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
-                        .onChange(of: state.glucoseFromPersistence.last?.glucose) { _ in
+                        .onChange(of: state.glucoseFromPersistence.last?.glucose) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                             updateStartEndMarkers()
                         }
-                        .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) { _ in
+                        .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
                         }
-                        .onChange(of: units) { _ in
+                        .onChange(of: units) {
                             // TODO: - Refactor this to only update the Y Axis Scale
                             state.setupGlucoseArray()
                         }
@@ -224,7 +224,7 @@ extension MainChartView {
                 }
             }
             .id("MainChart")
-            .onChange(of: state.insulinFromPersistence) { _ in
+            .onChange(of: state.insulinFromPersistence) {
                 state.roundedTotalBolus = state.calculateTINS()
             }
             .frame(minHeight: geo.size.height * 0.28)

+ 9 - 10
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -2,16 +2,15 @@ import CoreData
 import SwiftUI
 
 struct CurrentGlucoseView: View {
-    @Binding var timerDate: Date
-    @Binding var units: GlucoseUnits
-    @Binding var alarm: GlucoseAlarm?
-    @Binding var lowGlucose: Decimal
-    @Binding var highGlucose: Decimal
-    @Binding var cgmAvailable: Bool
-    @Binding var currentGlucoseTarget: Decimal
-    @Binding var glucoseColorScheme: GlucoseColorScheme
-
-    var glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
+    let timerDate: Date
+    let units: GlucoseUnits
+    let alarm: GlucoseAlarm?
+    let lowGlucose: Decimal
+    let highGlucose: Decimal
+    let cgmAvailable: Bool
+    var currentGlucoseTarget: Decimal
+    let glucoseColorScheme: GlucoseColorScheme
+    let glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
 
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [

+ 6 - 6
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -8,13 +8,13 @@ struct LoopView: View {
         static let lag: TimeInterval = 30
     }
 
-    @Binding var closedLoop: Bool
-    @Binding var timerDate: Date
-    @Binding var isLooping: Bool
-    @Binding var lastLoopDate: Date
-    @Binding var manualTempBasal: Bool
+    let closedLoop: Bool
+    let timerDate: Date
+    let isLooping: Bool
+    let lastLoopDate: Date
+    let manualTempBasal: Bool
 
-    var determination: [OrefDetermination]
+    let determination: [OrefDetermination]
 
     private var dateFormatter: DateFormatter {
         let formatter = DateFormatter()

+ 7 - 7
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -2,13 +2,13 @@ import CoreData
 import SwiftUI
 
 struct PumpView: View {
-    @Binding var reservoir: Decimal?
-    @Binding var name: String
-    @Binding var expiresAtDate: Date?
-    @Binding var timerDate: Date
-    @Binding var timeZone: TimeZone?
-    @Binding var pumpStatusHighlightMessage: String?
-    @Binding var battery: [OpenAPS_Battery]
+    let reservoir: Decimal?
+    let name: String
+    let expiresAtDate: Date?
+    let timerDate: Date
+    let timeZone: TimeZone?
+    let pumpStatusHighlightMessage: String?
+    let battery: [OpenAPS_Battery]
 
     @Environment(\.colorScheme) var colorScheme
 

+ 35 - 34
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -8,7 +8,7 @@ extension Home {
     struct RootView: BaseView {
         let resolver: Resolver
 
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State var isStatusPopupPresented = false
         @State var showCancelAlert = false
         @State var showCancelConfirmDialog = false
@@ -52,7 +52,6 @@ extension Home {
             ascending: false,
             fetchLimit: 1
         )) var latestTempTarget: FetchedResults<TempTargetStored>
-
         var bolusProgressFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -127,14 +126,14 @@ extension Home {
 
         var glucoseView: some View {
             CurrentGlucoseView(
-                timerDate: $state.timerDate,
-                units: $state.units,
-                alarm: $state.alarm,
-                lowGlucose: $state.lowGlucose,
-                highGlucose: $state.highGlucose,
-                cgmAvailable: $state.cgmAvailable,
-                currentGlucoseTarget: $state.currentGlucoseTarget,
-                glucoseColorScheme: $state.glucoseColorScheme,
+                timerDate: state.timerDate,
+                units: state.units,
+                alarm: state.alarm,
+                lowGlucose: state.lowGlucose,
+                highGlucose: state.highGlucose,
+                cgmAvailable: state.cgmAvailable,
+                currentGlucoseTarget: state.currentGlucoseTarget,
+                glucoseColorScheme: state.glucoseColorScheme,
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
                 .onTapGesture {
@@ -149,13 +148,13 @@ extension Home {
 
         var pumpView: some View {
             PumpView(
-                reservoir: $state.reservoir,
-                name: $state.pumpName,
-                expiresAtDate: $state.pumpExpiresAtDate,
-                timerDate: $state.timerDate,
-                timeZone: $state.timeZone,
-                pumpStatusHighlightMessage: $state.pumpStatusHighlightMessage,
-                battery: $state.batteryFromPersistence
+                reservoir: state.reservoir,
+                name: state.pumpName,
+                expiresAtDate: state.pumpExpiresAtDate,
+                timerDate: state.timerDate,
+                timeZone: state.timeZone,
+                pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
+                battery: state.batteryFromPersistence
             ).onTapGesture {
                 if state.pumpDisplayState == nil {
                     // shows user confirmation dialog with pump model choices, then proceeds to setup
@@ -353,16 +352,17 @@ extension Home {
             ZStack {
                 MainChartView(
                     geo: geo,
-                    units: $state.units,
-                    hours: .constant(state.filteredHours),
-                    highGlucose: $state.highGlucose,
-                    lowGlucose: $state.lowGlucose,
-                    currentGlucoseTarget: $state.currentGlucoseTarget,
-                    screenHours: $state.hours,
-                    glucoseColorScheme: $state.glucoseColorScheme,
-                    displayXgridLines: $state.displayXgridLines,
-                    displayYgridLines: $state.displayYgridLines,
-                    thresholdLines: $state.thresholdLines,
+                    units: state.units,
+                    hours: state.filteredHours,
+                    tempTargets: state.tempTargets,
+                    highGlucose: state.highGlucose,
+                    lowGlucose: state.lowGlucose,
+                    currentGlucoseTarget: state.currentGlucoseTarget,
+                    glucoseColorScheme: state.glucoseColorScheme,
+                    screenHours: state.hours,
+                    displayXgridLines: state.displayXgridLines,
+                    displayYgridLines: state.displayYgridLines,
+                    thresholdLines: state.thresholdLines,
                     state: state
                 )
             }
@@ -379,11 +379,11 @@ extension Home {
             VStack(alignment: .leading, spacing: 20) {
                 /// Loop view at bottomLeading
                 LoopView(
-                    closedLoop: $state.closedLoop,
-                    timerDate: $state.timerDate,
-                    isLooping: $state.isLooping,
-                    lastLoopDate: $state.lastLoopDate,
-                    manualTempBasal: $state.manualTempBasal,
+                    closedLoop: state.closedLoop,
+                    timerDate: state.timerDate,
+                    isLooping: state.isLooping,
+                    lastLoopDate: state.lastLoopDate,
+                    manualTempBasal: state.manualTempBasal,
                     determination: state.determinationsFromPersistence
                 ).onTapGesture {
                     state.isStatusPopupPresented = true
@@ -445,8 +445,9 @@ extension Home {
                         .foregroundColor(.loopYellow)
                     Text(
                         (
-                            numberFormatter
-                                .string(from: (state.enactedAndNonEnactedDeterminations.first?.cob ?? 0) as NSNumber) ?? "0"
+                            numberFormatter.string(
+                                from: NSNumber(value: state.enactedAndNonEnactedDeterminations.first?.cob ?? 0)
+                            ) ?? "0"
                         ) +
                             NSLocalizedString(" g", comment: "gram of carbs")
                     )

+ 9 - 8
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -1,18 +1,19 @@
 import CoreData
+import Observation
 import SwiftUI
 
 extension ISFEditor {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var determinationStorage: DeterminationStorage!
-        @Injected() private var nightscout: NightscoutManager!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+        @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
 
-        @Published var items: [Item] = []
-        @Published var initialItems: [Item] = []
-        @Published var shouldDisplaySaving: Bool = false
+        var items: [Item] = []
+        var initialItems: [Item] = []
+        var shouldDisplaySaving: Bool = false
         private(set) var autosensISF: Decimal?
         private(set) var autosensRatio: Decimal = 0
-        @Published var autotune: Autotune?
-        @Published var determinationsFromPersistence: [OrefDetermination] = []
+        var autotune: Autotune?
+        var determinationsFromPersistence: [OrefDetermination] = []
 
         let context = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext

+ 1 - 1
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension ISFEditor {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var editMode = EditMode.inactive
 
         @Environment(\.colorScheme) var colorScheme

+ 5 - 4
FreeAPS/Sources/Modules/ManualTempBasal/ManualTempBasalStateModel.swift

@@ -1,10 +1,11 @@
+import Observation
 import SwiftUI
 
 extension ManualTempBasal {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var apsManager: APSManager!
-        @Published var rate: Decimal = 0
-        @Published var durationIndex = 0
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        var rate: Decimal = 0
+        var durationIndex = 0
 
         let durationValues = stride(from: 30.0, to: 720.1, by: 30.0).map { $0 }
 

+ 1 - 1
FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension ManualTempBasal {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {

+ 61 - 66
FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift

@@ -1,62 +1,64 @@
+import Combine
 import CoreData
+import Observation
 import SwiftUI
 
 extension OverrideConfig {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var broadcaster: Broadcaster!
-        @Injected() var tempTargetStorage: TempTargetsStorage!
-        @Injected() var apsManager: APSManager!
-        @Injected() var overrideStorage: OverrideStorage!
-
-        @Published var overrideSliderPercentage: Double = 100
-        @Published var isEnabled = false
-        @Published var indefinite = true
-        @Published var overrideDuration: Decimal = 0
-        @Published var target: Decimal = 0
-        @Published var shouldOverrideTarget: Bool = false
-        @Published var smbIsOff: Bool = false
-        @Published var id = ""
-        @Published var overrideName: String = ""
-        @Published var isPreset: Bool = false
-        @Published var overridePresets: [OverrideStored] = []
-        @Published var advancedSettings: Bool = false
-        @Published var isfAndCr: Bool = true
-        @Published var isf: Bool = true
-        @Published var cr: Bool = true
-        @Published var smbIsAlwaysOff: Bool = false
-        @Published var start: Decimal = 0
-        @Published var end: Decimal = 23
-        @Published var smbMinutes: Decimal = 0
-        @Published var uamMinutes: Decimal = 0
-        @Published var defaultSmbMinutes: Decimal = 0
-        @Published var defaultUamMinutes: Decimal = 0
-        @Published var selectedTab: Tab = .overrides
-        @Published var activeOverrideName: String = ""
-        @Published var activeTempTargetName: String = ""
-        @Published var currentActiveOverride: OverrideStored?
-        @Published var currentActiveTempTarget: TempTargetStored?
-        @Published var showOverrideEditSheet = false
-        @Published var showTempTargetEditSheet = false
-        @Published var showInvalidTargetAlert = false
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var storage: TempTargetsStorage!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+
+        var overridePercentage: Double = 100
+        var isEnabled = false
+        var indefinite = true
+        var overrideDuration: Decimal = 0
+        var target: Decimal = 100
+        var shouldOverrideTarget: Bool = false
+        var smbIsOff: Bool = false
+        var id = ""
+        var overrideName: String = ""
+        var isPreset: Bool = false
+        var overridePresets: [OverrideStored] = []
+        var advancedSettings: Bool = false
+        var isfAndCr: Bool = true
+        var isf: Bool = true
+        var cr: Bool = true
+        var smbIsScheduledOff: Bool = false
+        var start: Decimal = 0
+        var end: Decimal = 0
+        var smbMinutes: Decimal = 0
+        var uamMinutes: Decimal = 0
+        var defaultSmbMinutes: Decimal = 0
+        var defaultUamMinutes: Decimal = 0
+        var selectedTab: Tab = .overrides
+        var activeOverrideName: String = ""
+        var currentActiveOverride: OverrideStored?
+        var showOverrideEditSheet = false
+        var showTempTargetEditSheet = false
+        var currentActiveTempTarget: TempTargetStored?
+        var currentActiveOverride: OverrideStored?
+        var activeTempTargetName: String = ""
 
         var units: GlucoseUnits = .mgdL
 
         // temp target stuff
-        @Published var tempTargetDuration: Decimal = 0
-        @Published var tempTargetName: String = ""
-        @Published var tempTargetTarget: Decimal = 0 // lel
-        @Published var isTempTargetEnabled: Bool = false
-        @Published var date = Date()
-        @Published var newPresetName = ""
-        @Published var tempTargetPresets: [TempTargetStored] = []
-        @Published var percentage = 100.0
-        @Published var maxValue: Decimal = 1.2
-        @Published var minValue: Decimal = 0.15
-        @Published var viewPercantage = false
-        @Published var halfBasalTarget: Decimal = 160
-        @Published var settingHalfBasalTarget: Decimal = 160
-        @Published var didSaveSettings: Bool = false
-        @Published var didAdjustSens: Bool = false {
+        var tempTargetDuration: Decimal = 0
+        var tempTargetName: String = ""
+        var tempTargetTarget: Decimal = 0 // lel
+        var isTempTargetEnabled: Bool = false
+        var date = Date()
+        var newPresetName = ""
+        var tempTargetPresets: [TempTargetStored] = []
+        var percentage = 100.0
+        var maxValue: Decimal = 1.2
+        var minValue: Decimal = 0.15
+        var viewPercantage = false
+        var halfBasalTarget: Decimal = 160
+        var settingHalfBasalTarget: Decimal = 160
+        var didSaveSettings: Bool = false
+        var didAdjustSens: Bool = false {
             didSet {
                 handleAdjustSensToggle()
             }
@@ -70,6 +72,8 @@ extension OverrideConfig {
             return "Please enter a valid target between" + " \(target)."
         }
 
+        private var cancellables = Set<AnyCancellable>()
+
         override func subscribe() {
             setupNotification()
             setupSettings()
@@ -129,12 +133,12 @@ extension OverrideConfig {
 extension OverrideConfig.StateModel {
     // Custom Notification to update View when an Override has been cancelled via Home View
     func setupNotification() {
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleOverrideConfigurationUpdate),
-            name: .didUpdateOverrideConfiguration,
-            object: nil
-        )
+        Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.updateLatestOverrideConfiguration()
+            }
+            .store(in: &cancellables)
 
         // Custom Notification to update View when an Temp Target has been cancelled via Home View
         Foundation.NotificationCenter.default.addObserver(
@@ -144,15 +148,6 @@ extension OverrideConfig.StateModel {
             object: nil
         )
     }
-
-    @objc private func handleOverrideConfigurationUpdate() {
-        updateLatestOverrideConfiguration()
-    }
-
-    @objc private func handleTempTargetConfigurationUpdate() {
-        updateLatestTempTargetConfiguration()
-    }
-
     // MARK: - Enact Overrides
 
     func reorderOverride(from source: IndexSet, to destination: Int) {

+ 2 - 2
FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift

@@ -5,7 +5,7 @@ struct EditOverrideForm: View {
     @ObservedObject var override: OverrideStored
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
-    @StateObject var state: OverrideConfig.StateModel
+    @Bindable var state: OverrideConfig.StateModel
 
     @State private var name: String
     @State private var percentage: Double
@@ -30,7 +30,7 @@ struct EditOverrideForm: View {
 
     init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
         override = overrideToEdit
-        _state = StateObject(wrappedValue: state)
+        _state = Bindable(wrappedValue: state)
         _name = State(initialValue: overrideToEdit.name ?? "")
         _percentage = State(initialValue: overrideToEdit.percentage)
         _indefinite = State(initialValue: overrideToEdit.indefinite)

+ 1 - 1
FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift

@@ -6,7 +6,7 @@ extension OverrideConfig {
     struct RootView: BaseView {
         let resolver: Resolver
 
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
         @State private var isEditing = false
         @State private var showOverrideCreationSheet = false

+ 19 - 18
FreeAPS/Sources/Modules/SMBSettings/SMBSettingsStateModel.swift

@@ -1,26 +1,27 @@
+import Observation
 import SwiftUI
 
 extension SMBSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Injected() var storage: FileStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        @ObservationIgnored @Injected() var storage: FileStorage!
 
-        @Published var units: GlucoseUnits = .mgdL
+        var units: GlucoseUnits = .mgdL
 
-        @Published var enableSMBAlways: Bool = false
-        @Published var maxDeltaBGthreshold: Decimal = 0.2
-        @Published var enableSMBWithCOB: Bool = false
-        @Published var enableSMBWithTemptarget: Bool = false
-        @Published var enableSMBAfterCarbs: Bool = false
-        @Published var allowSMBWithHighTemptarget: Bool = false
-        @Published var enableSMB_high_bg: Bool = false
-        @Published var enableSMB_high_bg_target: Decimal = 100
-        @Published var maxSMBBasalMinutes: Decimal = 30
-        @Published var smbDeliveryRatio: Decimal = 0.5
-        @Published var smbInterval: Decimal = 3
-        @Published var bolusIncrement: Decimal = 0.1 // get this from pump, dafuq?: Bool = false
-        @Published var enableUAM: Bool = false
-        @Published var maxUAMSMBBasalMinutes: Decimal = 30
+        var enableSMBAlways: Bool = false
+        var maxDeltaBGthreshold: Decimal = 0.2
+        var enableSMBWithCOB: Bool = false
+        var enableSMBWithTemptarget: Bool = false
+        var enableSMBAfterCarbs: Bool = false
+        var allowSMBWithHighTemptarget: Bool = false
+        var enableSMB_high_bg: Bool = false
+        var enableSMB_high_bg_target: Decimal = 100
+        var maxSMBBasalMinutes: Decimal = 30
+        var smbDeliveryRatio: Decimal = 0.5
+        var smbInterval: Decimal = 3
+        var bolusIncrement: Decimal = 0.1 // get this from pump, dafuq?: Bool = false
+        var enableUAM: Bool = false
+        var maxUAMSMBBasalMinutes: Decimal = 30
 
         var preferences: Preferences {
             settingsManager.preferences

+ 1 - 1
FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension SMBSettings {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?

+ 5 - 4
FreeAPS/Sources/Modules/Snooze/SnoozeStateModel.swift

@@ -1,11 +1,12 @@
+import Observation
 import SwiftUI
 
 extension Snooze {
-    final class StateModel: BaseStateModel<Provider> {
-        @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast
-        @Injected() var glucoseStogare: GlucoseStorage!
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast
+        @ObservationIgnored @Injected() var glucoseStogare: GlucoseStorage!
 
-        @Published var alarm: GlucoseAlarm?
+        var alarm: GlucoseAlarm?
 
         override func subscribe() {
             alarm = glucoseStogare.alarm

+ 1 - 1
FreeAPS/Sources/Modules/Snooze/View/SnoozeRootView.swift

@@ -5,7 +5,7 @@ import Swinject
 extension Snooze {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {

+ 10 - 9
FreeAPS/Sources/Modules/Stat/StatStateModel.swift

@@ -1,19 +1,20 @@
 import CoreData
 import Foundation
+import Observation
 import SwiftUI
 import Swinject
 
 extension Stat {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
-        @Published var highLimit: Decimal = 10 / 0.0555
-        @Published var lowLimit: Decimal = 4 / 0.0555
-        @Published var overrideUnit: Bool = false
-        @Published var layingChart: Bool = false
-        @Published var units: GlucoseUnits = .mgdL
-        @Published var glucoseFromPersistence: [GlucoseStored] = []
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        @ObservationIgnored @Injected() var settings: SettingsManager!
+        var highLimit: Decimal = 10 / 0.0555
+        var lowLimit: Decimal = 4 / 0.0555
+        var overrideUnit: Bool = false
+        var layingChart: Bool = false
+        var units: GlucoseUnits = .mgdL
+        var glucoseFromPersistence: [GlucoseStored] = []
 
-        @Published var selectedDuration: Duration = .Today
+        var selectedDuration: Duration = .Today
 
         private let context = CoreDataStack.shared.newTaskContext()
         private let viewContext = CoreDataStack.shared.persistentContainer.viewContext

+ 16 - 16
FreeAPS/Sources/Modules/Stat/View/ChartsView.swift

@@ -4,13 +4,13 @@ import SwiftDate
 import SwiftUI
 
 struct ChartsView: View {
-    @Binding var highLimit: Decimal
-    @Binding var lowLimit: Decimal
-    @Binding var units: GlucoseUnits
-    @Binding var overrideUnit: Bool
-    @Binding var standing: Bool
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let overrideUnit: Bool
+    let standing: Bool
 
-    var glucose: [GlucoseStored]
+    let glucose: [GlucoseStored]
 
     @State var headline: Color = .secondary
 
@@ -36,18 +36,18 @@ struct ChartsView: View {
     }
 
     init(
-        _ highLimit: Binding<Decimal>,
-        _ lowLimit: Binding<Decimal>,
-        _ units: Binding<GlucoseUnits>,
-        _ overrideUnit: Binding<Bool>,
-        _ standing: Binding<Bool>,
+        highLimit: Decimal,
+        lowLimit: Decimal,
+        units: GlucoseUnits,
+        overrideUnit: Bool,
+        standing: Bool,
         glucose: [GlucoseStored]
     ) {
-        _highLimit = highLimit
-        _lowLimit = lowLimit
-        _units = units
-        _overrideUnit = overrideUnit
-        _standing = standing
+        self.highLimit = highLimit
+        self.lowLimit = lowLimit
+        self.units = units
+        self.overrideUnit = overrideUnit
+        self.standing = standing
         self.glucose = glucose
     }
 

+ 46 - 46
FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

@@ -7,7 +7,7 @@ import Swinject
 extension Stat {
     struct RootView: BaseView {
         let resolver: Resolver
-        @StateObject var state = StateModel()
+        @State var state = StateModel()
 
         @Environment(\.colorScheme) var colorScheme
 
@@ -42,42 +42,42 @@ extension Stat {
                 case .Today:
                     StatsView(
                         filter: filter.today,
-                        $state.highLimit,
-                        $state.lowLimit,
-                        $state.units,
-                        $state.overrideUnit
+                        highLimit: state.highLimit,
+                        lowLimit: state.lowLimit,
+                        units: state.units,
+                        overrideUnit: state.overrideUnit
                     )
                 case .Day:
                     StatsView(
                         filter: filter.day,
-                        $state.highLimit,
-                        $state.lowLimit,
-                        $state.units,
-                        $state.overrideUnit
+                        highLimit: state.highLimit,
+                        lowLimit: state.lowLimit,
+                        units: state.units,
+                        overrideUnit: state.overrideUnit
                     )
                 case .Week:
                     StatsView(
                         filter: filter.week,
-                        $state.highLimit,
-                        $state.lowLimit,
-                        $state.units,
-                        $state.overrideUnit
+                        highLimit: state.highLimit,
+                        lowLimit: state.lowLimit,
+                        units: state.units,
+                        overrideUnit: state.overrideUnit
                     )
                 case .Month:
                     StatsView(
                         filter: filter.month,
-                        $state.highLimit,
-                        $state.lowLimit,
-                        $state.units,
-                        $state.overrideUnit
+                        highLimit: state.highLimit,
+                        lowLimit: state.lowLimit,
+                        units: state.units,
+                        overrideUnit: state.overrideUnit
                     )
                 case .Total:
                     StatsView(
                         filter: filter.total,
-                        $state.highLimit,
-                        $state.lowLimit,
-                        $state.units,
-                        $state.overrideUnit
+                        highLimit: state.highLimit,
+                        lowLimit: state.lowLimit,
+                        units: state.units,
+                        overrideUnit: state.overrideUnit
                     )
                 }
             }
@@ -87,47 +87,47 @@ extension Stat {
             switch state.selectedDuration {
             case .Today:
                 ChartsView(
-                    $state.highLimit,
-                    $state.lowLimit,
-                    $state.units,
-                    $state.overrideUnit,
-                    $state.layingChart,
+                    highLimit: state.highLimit,
+                    lowLimit: state.lowLimit,
+                    units: state.units,
+                    overrideUnit: state.overrideUnit,
+                    standing: state.layingChart,
                     glucose: state.glucoseFromPersistence
                 )
             case .Day:
                 ChartsView(
-                    $state.highLimit,
-                    $state.lowLimit,
-                    $state.units,
-                    $state.overrideUnit,
-                    $state.layingChart,
+                    highLimit: state.highLimit,
+                    lowLimit: state.lowLimit,
+                    units: state.units,
+                    overrideUnit: state.overrideUnit,
+                    standing: state.layingChart,
                     glucose: state.glucoseFromPersistence
                 )
             case .Week:
                 ChartsView(
-                    $state.highLimit,
-                    $state.lowLimit,
-                    $state.units,
-                    $state.overrideUnit,
-                    $state.layingChart,
+                    highLimit: state.highLimit,
+                    lowLimit: state.lowLimit,
+                    units: state.units,
+                    overrideUnit: state.overrideUnit,
+                    standing: state.layingChart,
                     glucose: state.glucoseFromPersistence
                 )
             case .Month:
                 ChartsView(
-                    $state.highLimit,
-                    $state.lowLimit,
-                    $state.units,
-                    $state.overrideUnit,
-                    $state.layingChart,
+                    highLimit: state.highLimit,
+                    lowLimit: state.lowLimit,
+                    units: state.units,
+                    overrideUnit: state.overrideUnit,
+                    standing: state.layingChart,
                     glucose: state.glucoseFromPersistence
                 )
             case .Total:
                 ChartsView(
-                    $state.highLimit,
-                    $state.lowLimit,
-                    $state.units,
-                    $state.overrideUnit,
-                    $state.layingChart,
+                    highLimit: state.highLimit,
+                    lowLimit: state.lowLimit,
+                    units: state.units,
+                    overrideUnit: state.overrideUnit,
+                    standing: state.layingChart,
                     glucose: state.glucoseFromPersistence
                 )
             }

+ 12 - 12
FreeAPS/Sources/Modules/Stat/View/StatsView.swift

@@ -8,10 +8,10 @@ struct StatsView: View {
 
     @State var headline: Color = .secondary
 
-    @Binding var highLimit: Decimal
-    @Binding var lowLimit: Decimal
-    @Binding var units: GlucoseUnits
-    @Binding var overrideUnit: Bool
+    var highLimit: Decimal
+    var lowLimit: Decimal
+    var units: GlucoseUnits
+    var overrideUnit: Bool
 
     private let conversionFactor = 0.0555
 
@@ -27,10 +27,10 @@ struct StatsView: View {
 
     init(
         filter: NSDate,
-        _ highLimit: Binding<Decimal>,
-        _ lowLimit: Binding<Decimal>,
-        _ units: Binding<GlucoseUnits>,
-        _ overrideUnit: Binding<Bool>
+        highLimit: Decimal,
+        lowLimit: Decimal,
+        units: GlucoseUnits,
+        overrideUnit: Bool
     ) {
         _fetchRequest = FetchRequest<LoopStatRecord>(
             sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)],
@@ -42,10 +42,10 @@ struct StatsView: View {
             predicate: NSPredicate(format: "glucose > 0 AND date > %@", filter)
         )
 
-        _highLimit = highLimit
-        _lowLimit = lowLimit
-        _units = units
-        _overrideUnit = overrideUnit
+        self.highLimit = highLimit
+        self.lowLimit = lowLimit
+        self.units = units
+        self.overrideUnit = overrideUnit
     }
 
     var loops: some View {

+ 31 - 31
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -341,15 +341,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         await uploadInsulin(pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth())
     }
 
-    func uploadInsulin(_ insulin: [PumpHistoryEvent]) async {
+    func uploadInsulin(_ insulinEvents: [PumpHistoryEvent]) async {
         guard settingsManager.settings.useAppleHealth,
               let sampleType = AppleHealthConfig.healthInsulinObject,
               checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
-              insulin.isNotEmpty
-        else { return }
+              insulinEvents.isNotEmpty else { return }
 
         // Fetch existing temp basal entries from Core Data for the last 24 hours
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let fetchedInsulinEntries = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: backgroundContext,
             predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
@@ -361,21 +360,12 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             batchSize: 50
         )
 
-        // Initialize an array to hold the HealthKit samples to be uploaded
         var insulinSamples: [HKQuantitySample] = []
 
-        // Perform the data processing on the background context
         await backgroundContext.perform {
-            // Ensure that the fetched results are of the expected type
-            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
-
-            // Create a mapping from timestamps to indices for quick access to existing entries
-            let existingEntriesByTimestamp = Dictionary(
-                uniqueKeysWithValues: existingTempBasalEntries.enumerated()
-                    .map { ($0.element.timestamp, $0.offset) }
-            )
+            guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else { return }
 
-            for event in insulin {
+            for event in insulinEvents {
                 switch event.type {
                 case .bolus:
                     // For bolus events, create a HealthKit sample directly
@@ -383,24 +373,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                         debug(.service, "Created HealthKit sample for bolus entry: \(sample)")
                         insulinSamples.append(sample)
                     }
-
                 case .tempBasal:
                     // For temp basal events, process them and adjust overlapping durations if necessary
                     guard let duration = event.duration, let amount = event.amount else { continue }
 
-                    // Calculate the total insulin delivered during the temp basal period
                     let value = (Decimal(duration) / 60.0) * amount
                     let valueRounded = self.deviceDataManager?.pumpManager?
                         .roundToSupportedBolusVolume(units: Double(value)) ?? Double(value)
 
-                    // Check if there's a matching existing temp basal entry
-                    if let matchingEntryIndex = existingEntriesByTimestamp[event.timestamp] {
-                        let predecessorIndex = matchingEntryIndex - 1
+                    // Use binary search for efficient lookup of matching entry
+                    if let matchingIndex = self.binarySearch(entries: existingTempBasalEntries, timestamp: event.timestamp) {
+                        let predecessorIndex = matchingIndex - 1
+
                         if predecessorIndex >= 0 {
-                            // Get the predecessor entry to handle overlapping temp basal events
                             let predecessorEntry = existingTempBasalEntries[predecessorIndex]
 
-                            // Adjust the predecessor entry if it overlaps with the current event
                             if let adjustedSample = self.processPredecessorEntry(
                                 predecessorEntry,
                                 nextEventTimestamp: event.timestamp,
@@ -410,7 +397,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                             }
                         }
 
-                        // Create a new PumpHistoryEvent with the calculated insulin value
                         let newEvent = PumpHistoryEvent(
                             id: event.id,
                             type: .tempBasal,
@@ -419,7 +405,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                             duration: event.duration
                         )
 
-                        // Create a HealthKit sample for the current temp basal event
                         if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
                             debug(.service, "Created HealthKit sample for initial temp basal entry: \(sample)")
                             insulinSamples.append(sample)
@@ -427,31 +412,46 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                     }
 
                 default:
-                    // Ignore other event types
                     break
                 }
             }
         }
 
-        // Save the processed insulin samples to HealthKit
         do {
             guard insulinSamples.isNotEmpty else {
                 debug(.service, "No insulin samples available for upload.")
                 return
             }
 
-            // Attempt to save the samples to HealthKit
             try await healthKitStore.save(insulinSamples)
             debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
-
-            // Mark the insulin events as uploaded
-            await updateInsulinAsUploaded(insulin)
-
+            await updateInsulinAsUploaded(insulinEvents)
         } catch {
             debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
         }
     }
 
+    // Helper function to perform binary search on the sorted entries by timestamp
+    private func binarySearch(entries: [PumpEventStored], timestamp: Date) -> Int? {
+        var lowerBound = 0
+        var upperBound = entries.count - 1
+
+        while lowerBound <= upperBound {
+            let midIndex = (lowerBound + upperBound) / 2
+            guard let midTimestamp = entries[midIndex].timestamp else { return nil }
+
+            if midTimestamp == timestamp {
+                return midIndex
+            } else if midTimestamp < timestamp {
+                lowerBound = midIndex + 1
+            } else {
+                upperBound = midIndex - 1
+            }
+        }
+
+        return nil
+    }
+
     // Helper function to create a HealthKit sample from a PumpHistoryEvent
     private func createSample(
         for event: PumpHistoryEvent,

+ 16 - 15
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -68,14 +68,17 @@ import UIKit
 
     private func setupNotifications() {
         let notificationCenter = Foundation.NotificationCenter.default
-        notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
         notificationCenter
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
-                self?.forceActivityUpdate()
+                Task { @MainActor in
+                    self?.forceActivityUpdate()
+                }
             }
         notificationCenter
             .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
-                self?.forceActivityUpdate()
+                Task { @MainActor in
+                    self?.forceActivityUpdate()
+                }
             }
     }
 
@@ -85,6 +88,11 @@ import UIKit
             guard let self = self else { return }
             self.overridesDidUpdate()
         }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+            guard let self = self else { return }
+            self.cobOrIobDidUpdate()
+        }.store(in: &subscriptions)
     }
 
     private func registerSubscribers() {
@@ -97,7 +105,7 @@ import UIKit
             .store(in: &subscriptions)
     }
 
-    @objc private func cobOrIobDidUpdate() {
+    private func cobOrIobDidUpdate() {
         Task {
             await fetchAndMapDetermination()
             if let determination = determination {
@@ -106,7 +114,7 @@ import UIKit
         }
     }
 
-    @objc private func overridesDidUpdate() {
+    private func overridesDidUpdate() {
         Task {
             await fetchAndMapOverride()
             if let determination = determination {
@@ -120,15 +128,8 @@ import UIKit
             // Fetch and map glucose to GlucoseData struct
             await fetchAndMapGlucose()
 
-            // Fetch and map Determination to DeterminationData struct
-            await fetchAndMapDetermination()
-
-            // Fetch and map Override to OverrideData struct
-            /// shows if there is an active Override
-            await fetchAndMapOverride()
-
             // Push the update to the Live Activity
-            glucoseDidUpdate(glucoseFromPersistence ?? [])
+            await glucoseDidUpdate(glucoseFromPersistence ?? [])
         }
     }
 
@@ -146,7 +147,7 @@ import UIKit
 
     /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
     /// Ends existing live activities if live activities are not enabled in settings
-    private func forceActivityUpdate() {
+    @MainActor private func forceActivityUpdate() {
         // just before app resigns active, show a new activity
         // only do this if there is no current activity or the current activity is older than 1h
         if settings.useLiveActivity {
@@ -251,7 +252,7 @@ import UIKit
 
 @available(iOS 16.2, *)
 extension LiveActivityBridge {
-    func glucoseDidUpdate(_ glucose: [GlucoseData]) {
+    @MainActor func glucoseDidUpdate(_ glucose: [GlucoseData]) {
         guard settings.useLiveActivity else {
             if currentActivity != nil {
                 Task {

+ 10 - 11
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -152,18 +152,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func setupNotification() {
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleOverrideConfigurationUpdate),
-            name: .didUpdateOverrideConfiguration,
-            object: nil
-        )
-    }
+        Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadOverrides()
 
-    @objc private func handleOverrideConfigurationUpdate() {
-        Task.detached {
-            await self.uploadOverrides()
-        }
+                    // Post a notification indicating that the upload has finished and that we can end the background task in the OverridePresetsIntentRequest
+                    Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
+                }
+            }
+            .store(in: &subscriptions)
     }
 
     func sourceInfo() -> [String: Any]? {

+ 108 - 57
FreeAPS/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift

@@ -1,5 +1,6 @@
 import CoreData
 import Foundation
+import UIKit
 
 @available(iOS 16.0, *) final class OverridePresetsIntentRequest: BaseIntentsRequest {
     enum overridePresetsError: Error {
@@ -77,95 +78,145 @@ import Foundation
     }
 
     @MainActor func enactOverride(_ preset: OverridePreset) async -> Bool {
+        // Start background task
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+        backgroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Override Upload") {
+            guard backgroundTaskID != .invalid else { return }
+            Task {
+                // End background task when the time is about to expire
+                await UIApplication.shared.endBackgroundTask(backgroundTaskID)
+            }
+            backgroundTaskID = .invalid
+        }
+
+        // Defer block to end background task when function exits
+        defer {
+            if backgroundTaskID != .invalid {
+                Task {
+                    await UIApplication.shared.endBackgroundTask(backgroundTaskID)
+                }
+                backgroundTaskID = .invalid
+            }
+        }
+
         do {
+            // Get NSManagedObjectID of Preset
             guard let overrideID = await fetchOverrideID(preset),
-                  let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored else { return false }
+                  let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored
+            else { return false }
 
+            // Enable Override
             overrideObject.enabled = true
             overrideObject.date = Date()
             overrideObject.isUploadedToNS = false
 
-            // Disable previous Overrides
-            /// do not create a OverrideRunEntry because we only want that if we cancel a running Override, not when enacting a Preset
-            await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true)
+            // Disable previous overrides if necessary, without starting a background task
+            await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true, shouldStartBackgroundTask: false)
 
             if viewContext.hasChanges {
                 try viewContext.save()
 
                 // Update State variables in OverrideView
-                Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+
+                // Await the notification
+                print("Waiting for notification...")
+                await awaitNotification(.didUpdateOverrideConfiguration)
+                print("Notification received, continuing...")
 
                 return true
             }
-        } catch let error as NSError {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override: \(error.localizedDescription)"
-            )
+        } catch {
+            // Handle error and ensure background task is ended
+            debugPrint("Failed to enact Override: \(error.localizedDescription)")
         }
+
         return false
     }
 
     func cancelOverride() async {
-        await disableAllActiveOverrides(createOverrideRunEntry: true)
+        await disableAllActiveOverrides(createOverrideRunEntry: true, shouldStartBackgroundTask: true)
     }
 
     @MainActor func disableAllActiveOverrides(
         except overrideID: NSManagedObjectID? = nil,
-        createOverrideRunEntry _: Bool
+        createOverrideRunEntry: Bool,
+        shouldStartBackgroundTask: Bool = true
     ) async {
-        // Get ALL NSManagedObject IDs of ALL active Overrides to cancel every single Override
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
-        await viewContext.perform {
-            do {
-                // Fetch the existing OverrideStored objects from the context
-                let results = try ids.compactMap { id in
-                    try self.viewContext.existingObject(with: id) as? OverrideStored
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+
+        if shouldStartBackgroundTask {
+            // Start background task
+            backgroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Override Cancel") {
+                guard backgroundTaskID != .invalid else { return }
+                Task {
+                    // End background task when the time is about to expire
+                    await UIApplication.shared.endBackgroundTask(backgroundTaskID)
                 }
+                backgroundTaskID = .invalid
+            }
+        }
 
-                // If there are no results, return early
-                guard !results.isEmpty else { return }
-
-                // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
-                // Auggie - commented out this if statment, we always need to do this for overrides
-                // if createOverrideRunEntry {
-                // Use the first override to create a new OverrideRunStored entry
-                if let canceledOverride = results.first {
-                    let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
-                    newOverrideRunStored.id = UUID()
-                    newOverrideRunStored.name = canceledOverride.name
-                    newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
-                    newOverrideRunStored.endDate = Date()
-                    newOverrideRunStored
-                        .target = NSDecimalNumber(
-                            decimal: self.overrideStorage
-                                .calculateTarget(override: canceledOverride)
-                        )
-                    newOverrideRunStored.override = canceledOverride
-                    newOverrideRunStored.isUploadedToNS = false
-                }
-                // }
-
-                // Disable all override except the one with overrideID
-                for overrideToCancel in results {
-                    if overrideToCancel.objectID != overrideID {
-                        overrideToCancel.enabled = false
-                        overrideToCancel.isUploadedToNS = false
-                    }
+        // Defer block to end background task when function exits, only if it was started
+        defer {
+            if shouldStartBackgroundTask, backgroundTaskID != .invalid {
+                Task {
+                    await UIApplication.shared.endBackgroundTask(backgroundTaskID)
                 }
+                backgroundTaskID = .invalid
+            }
+        }
 
-                // Save the context if there are changes
-                if self.viewContext.hasChanges {
-                    try self.viewContext.save()
+        // Get NSManagedObjectID of all active overrides
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
-                    // Update State variables in OverrideView
-                    Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
-                }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+        do {
+            // Fetch existing OverrideStored objects
+            let results = try ids.compactMap { id in
+                try self.viewContext.existingObject(with: id) as? OverrideStored
+            }
+
+            // Return early if no results
+            guard !results.isEmpty else { return }
+
+            // Create OverrideRunStored entry if needed
+            if createOverrideRunEntry, let canceledOverride = results.first {
+                let newOverrideRunStored = OverrideRunStored(context: viewContext)
+                newOverrideRunStored.id = UUID()
+                newOverrideRunStored.name = canceledOverride.name
+                newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
+                newOverrideRunStored.endDate = Date()
+                newOverrideRunStored.target = NSDecimalNumber(
+                    decimal: overrideStorage.calculateTarget(override: canceledOverride)
                 )
+                newOverrideRunStored.override = canceledOverride
+                newOverrideRunStored.isUploadedToNS = false
             }
+
+            // Disable all overrides except the one specified
+            for overrideToCancel in results {
+                if overrideToCancel.objectID != overrideID {
+                    overrideToCancel.enabled = false
+                    overrideToCancel.isUploadedToNS = false
+                }
+            }
+
+            if viewContext.hasChanges {
+                try viewContext.save()
+
+                // Update State variables in OverrideView
+                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+            }
+
+            // Await the notification
+            print("Waiting for notification...")
+            await awaitNotification(.didUpdateOverrideConfiguration)
+            print("Notification received, continuing...")
+
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+            )
         }
     }
 }

+ 6 - 2
FreeAPS/Sources/Views/TextFieldWithToolBar.swift

@@ -225,10 +225,14 @@ extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
                 let hasTrailingZeros = (hasDecimalSeparator && proposedText[lastCharIndex] == "0") || isDecimalSeparator
                 if !hasTrailingZeros
                 {
-                    parent.text = number.decimalValue
+                    DispatchQueue.main.async {
+                        self.parent.text = number.decimalValue
+                    }
                 }
             } else {
-                parent.text = 0
+                DispatchQueue.main.async {
+                    self.parent.text = 0
+                }
             }
         }
 

+ 19 - 0
Model/Helper/CustomNotification.swift

@@ -1,7 +1,26 @@
+import Combine
 import Foundation
 
 extension Notification.Name {
+    static let willUpdateOverrideConfiguration = Notification.Name("willUpdateOverrideConfiguration")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
     static let didUpdateTempTargetConfiguration = Notification.Name("didUpdateTempTargetConfiguration")
     static let didUpdateCobIob = Notification.Name("didUpdateCobIob")
 }
+
+func awaitNotification(_ name: Notification.Name) async {
+    await withCheckedContinuation { continuation in
+        var cancellable: AnyCancellable?
+
+        // Create a Combine publisher that listens for notifications
+        cancellable = Foundation.NotificationCenter.default
+            .publisher(for: name)
+            .sink { _ in
+                // When the notification is received, resume the awaiting task
+                continuation.resume()
+
+                // Cancel the subscription after the continuation has resumed
+                cancellable?.cancel()
+            }
+    }
+}