Selaa lähdekoodia

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into tempTargets

polscm32 aka Marvout 1 vuosi sitten
vanhempi
commit
b1eca2942e
33 muutettua tiedostoa jossa 735 lisäystä ja 281 poistoa
  1. 78 0
      FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS.xcscheme
  2. 2 2
      FreeAPS/Resources/Info.plist
  3. 7 0
      FreeAPS/Sources/APS/APSManager.swift
  4. 4 0
      FreeAPS/Sources/Logger/Logger.swift
  5. 2 2
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  6. 44 0
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  7. 96 25
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  8. 73 15
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  9. 2 0
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  10. 86 0
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  11. 43 26
      FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  12. 7 8
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  13. 98 31
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  14. 2 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  15. 2 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  16. 2 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  17. 8 10
      FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift
  18. 14 24
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  19. 42 26
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  20. 1 4
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift
  21. 13 9
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift
  22. 4 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift
  23. 38 21
      FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  24. 2 2
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  25. 3 2
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  26. 1 0
      FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift
  27. 1 1
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  28. 1 1
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  29. 29 46
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  30. 8 4
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  31. 21 17
      Gemfile.lock
  32. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  33. 0 1
      Model/CoreDataStack.swift

+ 78 - 0
FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS.xcscheme

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1600"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "388E595725AD948C0019842D"
+               BuildableName = "FreeAPS.app"
+               BlueprintName = "FreeAPS"
+               ReferencedContainer = "container:FreeAPS.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "388E595725AD948C0019842D"
+            BuildableName = "FreeAPS.app"
+            BlueprintName = "FreeAPS"
+            ReferencedContainer = "container:FreeAPS.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "388E595725AD948C0019842D"
+            BuildableName = "FreeAPS.app"
+            BlueprintName = "FreeAPS"
+            ReferencedContainer = "container:FreeAPS.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 2 - 2
FreeAPS/Resources/Info.plist

@@ -2,8 +2,6 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <plist version="1.0">
 <dict>
 <dict>
-	<key>TeamID</key>
-	<string>$(DEVELOPER_TEAM)</string>
 	<key>AppGroupID</key>
 	<key>AppGroupID</key>
 	<string>$(APP_GROUP_ID)</string>
 	<string>$(APP_GROUP_ID)</string>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
@@ -88,6 +86,8 @@
 	<string>$(COPYRIGHT_NOTICE)</string>
 	<string>$(COPYRIGHT_NOTICE)</string>
 	<key>NSSupportsLiveActivities</key>
 	<key>NSSupportsLiveActivities</key>
 	<true/>
 	<true/>
+	<key>TeamID</key>
+	<string>$(DEVELOPMENT_TEAM)</string>
 	<key>UIApplicationSceneManifest</key>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>
 		<key>UIApplicationSupportsMultipleScenes</key>

+ 7 - 0
FreeAPS/Sources/APS/APSManager.swift

@@ -273,6 +273,13 @@ final class BaseAPSManager: APSManager, Injectable {
                 await loopCompleted(error: error, loopStatRecord: loopStatRecord)
                 await loopCompleted(error: error, loopStatRecord: loopStatRecord)
             }
             }
 
 
+            if let nightscoutManager = nightscout {
+                await nightscoutManager.uploadCarbs()
+                await nightscoutManager.uploadPumpHistory()
+                await nightscoutManager.uploadOverrides()
+                await nightscoutManager.uploadTempTargets()
+            }
+
             // End background task after all the operations are completed
             // End background task after all the operations are completed
             if let backgroundTask = self.backGroundTaskID {
             if let backgroundTask = self.backGroundTaskID {
                 await UIApplication.shared.endBackgroundTask(backgroundTask)
                 await UIApplication.shared.endBackgroundTask(backgroundTask)

+ 4 - 0
FreeAPS/Sources/Logger/Logger.swift

@@ -113,6 +113,7 @@ final class Logger {
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
     static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
     static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
+    static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
 
 
     enum Category: String {
     enum Category: String {
         case `default`
         case `default`
@@ -123,6 +124,7 @@ final class Logger {
         case apsManager
         case apsManager
         case nightscout
         case nightscout
         case remoteControl
         case remoteControl
+        case bolusState
 
 
         var name: String {
         var name: String {
             rawValue.capitalizingFirstLetter()
             rawValue.capitalizingFirstLetter()
@@ -138,6 +140,7 @@ final class Logger {
             case .apsManager: return .apsManager
             case .apsManager: return .apsManager
             case .nightscout: return .nightscout
             case .nightscout: return .nightscout
             case .remoteControl: return .remoteControl
             case .remoteControl: return .remoteControl
+            case .bolusState: return .bolusState
             }
             }
         }
         }
 
 
@@ -146,6 +149,7 @@ final class Logger {
             switch self {
             switch self {
             case .default: return OSLog.default
             case .default: return OSLog.default
             case .apsManager,
             case .apsManager,
+                 .bolusState,
                  .businessLogic,
                  .businessLogic,
                  .deviceManager,
                  .deviceManager,
                  .nightscout,
                  .nightscout,

+ 2 - 2
FreeAPS/Sources/Models/DecimalPickerSettings.swift

@@ -33,8 +33,8 @@ class PickerSettingsProvider: ObservableObject {
 }
 }
 
 
 struct DecimalPickerSettings {
 struct DecimalPickerSettings {
-    var lowGlucose = PickerSetting(value: 72, step: 1, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
-    var highGlucose = PickerSetting(value: 270, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
+    var lowGlucose = PickerSetting(value: 70, step: 5, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
+    var highGlucose = PickerSetting(value: 180, step: 5, min: 100, max: 400, type: PickerSetting.PickerSettingType.glucose)
     var carbsRequiredThreshold = PickerSetting(value: 10, step: 1, min: 0, max: 100, type: PickerSetting.PickerSettingType.gramms)
     var carbsRequiredThreshold = PickerSetting(value: 10, step: 1, min: 0, max: 100, type: PickerSetting.PickerSettingType.gramms)
     var individualAdjustmentFactor = PickerSetting(
     var individualAdjustmentFactor = PickerSetting(
         value: 0.5,
         value: 0.5,

+ 44 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -10,6 +10,7 @@ extension BasalProfileEditor {
         var items: [Item] = []
         var items: [Item] = []
         var total: Decimal = 0.0
         var total: Decimal = 0.0
         var showAlert: Bool = false
         var showAlert: Bool = false
+        var chartData: [BasalProfile]? = []
 
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
 
@@ -114,6 +115,49 @@ extension BasalProfileEditor {
                 if self.items != sorted {
                 if self.items != sorted {
                     self.items = sorted
                     self.items = sorted
                 }
                 }
+                self.calcTotal()
+            }
+        }
+
+        func availableTimeIndices(_ itemIndex: Int) -> [Int] {
+            // avoid index out of range issues
+            guard itemIndex >= 0, itemIndex < items.count else {
+                return []
+            }
+
+            let usedIndicesByOtherItems = items
+                .enumerated()
+                .filter { $0.offset != itemIndex }
+                .map(\.element.timeIndex)
+
+            return (0 ..< timeValues.count).filter { !usedIndicesByOtherItems.contains($0) }
+        }
+
+        func caluclateChartData() {
+            DispatchQueue.main.async {
+                var basals: [BasalProfile] = []
+                let tzOffset = TimeZone.current.secondsFromGMT() * -1
+
+                basals.append(contentsOf: self.items.enumerated().map { index, item in
+                    let startDate = Date(timeIntervalSinceReferenceDate: self.timeValues[item.timeIndex])
+                    var endDate = Date(timeIntervalSinceReferenceDate: self.timeValues.last!).addingTimeInterval(30 * 60)
+                    if self.items.count > index + 1 {
+                        let nextItem = self.items[index + 1]
+                        endDate = Date(timeIntervalSinceReferenceDate: self.timeValues[nextItem.timeIndex])
+                    }
+
+                    return BasalProfile(
+                        amount: Double(self.rateValues[item.rateIndex]),
+                        isOverwritten: false,
+                        startDate: startDate.addingTimeInterval(TimeInterval(tzOffset)),
+                        endDate: endDate.addingTimeInterval(TimeInterval(tzOffset))
+                    )
+                })
+                basals.sort(by: {
+                    $0.startDate > $1.startDate
+                })
+
+                self.chartData = basals
             }
             }
         }
         }
     }
     }

+ 96 - 25
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -1,3 +1,4 @@
+import Charts
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -7,6 +8,9 @@ extension BasalProfileEditor {
         @State var state = StateModel()
         @State var state = StateModel()
         @State private var editMode = EditMode.inactive
         @State private var editMode = EditMode.inactive
 
 
+        let chartScale = Calendar.current
+            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
         var color: LinearGradient {
             colorScheme == .dark ? LinearGradient(
             colorScheme == .dark ? LinearGradient(
@@ -38,11 +42,92 @@ extension BasalProfileEditor {
             return formatter
             return formatter
         }
         }
 
 
-        var body: some View {
-            Form {
+        var basalScheduleChart: some View {
+            Chart {
+                ForEach(state.chartData!, id: \.self) { profile in
+                    RectangleMark(
+                        xStart: .value("start", profile.startDate),
+                        xEnd: .value("end", profile.endDate!),
+                        yStart: .value("rate-start", profile.amount),
+                        yEnd: .value("rate-end", 0)
+                    ).foregroundStyle(
+                        .linearGradient(
+                            colors: [
+                                Color.insulin.opacity(0.6),
+                                Color.insulin.opacity(0.1)
+                            ],
+                            startPoint: .bottom,
+                            endPoint: .top
+                        )
+                    ).alignsMarkStylesWithPlotArea()
+
+                    LineMark(x: .value("End Date", profile.endDate!), y: .value("Amount", profile.amount))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+
+                    LineMark(x: .value("Start Date", profile.startDate), y: .value("Amount", profile.amount))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+                }
+            }
+            .chartXAxis {
+                AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                    AxisValueLabel(format: .dateTime.hour())
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartYAxis {
+                AxisMarks(values: .automatic(desiredCount: 2)) { _ in
+                    AxisValueLabel()
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
+        }
+
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.syncInProgress || state.items.isEmpty || !state.hasChanges
                 let shouldDisableButton = state.syncInProgress || state.items.isEmpty || !state.hasChanges
 
 
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Group {
+                    HStack {
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+                        } label: {
+                            HStack {
+                                if state.syncInProgress {
+                                    ProgressView().padding(.trailing, 10)
+                                }
+                                Text(state.syncInProgress ? "Saving..." : "Save")
+                            }.padding(10)
+                        }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                        .disabled(shouldDisableButton)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                        .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
                 Section(header: Text("Schedule")) {
                 Section(header: Text("Schedule")) {
+                    if !state.items.isEmpty {
+                        basalScheduleChart.padding(.vertical)
+                    }
+
                     list
                     list
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
 
 
@@ -58,25 +143,8 @@ extension BasalProfileEditor {
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
                     }
                     }
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        if state.syncInProgress {
-                            ProgressView().padding(.trailing, 10)
-                        }
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
-                        } label: {
-                            Text(state.syncInProgress ? "Saving..." : "Save")
-                        }
-                        .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .alert(isPresented: $state.showAlert) {
             .alert(isPresented: $state.showAlert) {
                 Alert(
                 Alert(
                     title: Text("Unable to Save"),
                     title: Text("Unable to Save"),
@@ -84,11 +152,11 @@ extension BasalProfileEditor {
                     dismissButton: .default(Text("Close"))
                     dismissButton: .default(Text("Close"))
                 )
                 )
             }
             }
-            .onChange(of: state.items) { _ in
+            .onChange(of: state.items) {
                 state.calcTotal()
                 state.calcTotal()
+                state.caluclateChartData()
             }
             }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
             .navigationTitle("Basal Profile")
             .navigationTitle("Basal Profile")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
             .toolbar(content: {
@@ -101,7 +169,9 @@ extension BasalProfileEditor {
             })
             })
             .environment(\.editMode, $editMode)
             .environment(\.editMode, $editMode)
             .onAppear {
             .onAppear {
+                configureView()
                 state.validate()
                 state.validate()
+                state.caluclateChartData()
             }
             }
         }
         }
 
 
@@ -118,12 +188,12 @@ extension BasalProfileEditor {
                             ).tag(i)
                             ).tag(i)
                         }
                         }
                     }
                     }
-                    .onChange(of: state.items[index].rateIndex, perform: { _ in state.calcTotal() })
+                    .onChange(of: state.items[index].rateIndex, { state.calcTotal() })
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
 
 
                 Section {
                 Section {
                     Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
                     Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
+                        ForEach(state.availableTimeIndices(index), id: \.self) { i in
                             Text(
                             Text(
                                 self.dateFormatter
                                 self.dateFormatter
                                     .string(from: Date(
                                     .string(from: Date(
@@ -133,7 +203,7 @@ extension BasalProfileEditor {
                             ).tag(i)
                             ).tag(i)
                         }
                         }
                     }
                     }
-                    .onChange(of: state.items[index].timeIndex, perform: { _ in state.calcTotal() })
+                    .onChange(of: state.items[index].timeIndex, { state.calcTotal() })
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
             }
             }
             .padding(.top)
             .padding(.top)
@@ -185,6 +255,7 @@ extension BasalProfileEditor {
             state.items.remove(atOffsets: offsets)
             state.items.remove(atOffsets: offsets)
             state.validate()
             state.validate()
             state.calcTotal()
             state.calcTotal()
+            state.caluclateChartData()
         }
         }
     }
     }
 }
 }

+ 73 - 15
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -119,12 +119,24 @@ extension Bolus {
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
 
 
+        var isActive: Bool = false
+
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var subscriptions = Set<AnyCancellable>()
         private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
+        func unsubscribe() {
+            subscriptions.forEach { $0.cancel() }
+            subscriptions.removeAll()
+        }
+
         override func subscribe() {
         override func subscribe() {
+            guard isActive else {
+                return
+            }
+
+            debug(.bolusState, "subscribe fired")
             coreDataPublisher =
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
                 changedObjectsOnManagedObjectContextDidSavePublisher()
                     .receive(on: DispatchQueue.global(qos: .background))
                     .receive(on: DispatchQueue.global(qos: .background))
@@ -135,7 +147,19 @@ extension Bolus {
             setupBolusStateConcurrently()
             setupBolusStateConcurrently()
         }
         }
 
 
+        deinit {
+            // Unregister from broadcaster
+            broadcaster.unregister(DeterminationObserver.self, observer: self)
+            broadcaster.unregister(BolusFailureObserver.self, observer: self)
+
+            // Cancel Combine subscriptions
+            unsubscribe()
+
+            debug(.bolusState, "Bolus.StateModel deinitialized")
+        }
+
         private func setupBolusStateConcurrently() {
         private func setupBolusStateConcurrently() {
+            debug(.bolusState, "setupBolusStateConcurrently fired")
             Task {
             Task {
                 await withTaskGroup(of: Void.self) { group in
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
                     group.addTask {
@@ -241,9 +265,10 @@ extension Bolus {
             let now = Date()
             let now = Date()
             let calendar = Calendar.current
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
             let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm"
             dateFormatter.timeZone = TimeZone.current
             dateFormatter.timeZone = TimeZone.current
 
 
+            let regexWithSeconds = #"^\d{2}:\d{2}:\d{2}$"#
+
             let entries: [(start: String, value: Decimal)]
             let entries: [(start: String, value: Decimal)]
 
 
             switch type {
             switch type {
@@ -262,6 +287,13 @@ extension Bolus {
             }
             }
 
 
             for (index, entry) in entries.enumerated() {
             for (index, entry) in entries.enumerated() {
+                // Dynamically set the format based on whether it matches the regex
+                if entry.start.range(of: regexWithSeconds, options: .regularExpression) != nil {
+                    dateFormatter.dateFormat = "HH:mm:ss"
+                } else {
+                    dateFormatter.dateFormat = "HH:mm"
+                }
+
                 guard let entryTime = dateFormatter.date(from: entry.start) else {
                 guard let entryTime = dateFormatter.date(from: entry.start) else {
                     print("Invalid entry start time: \(entry.start)")
                     print("Invalid entry start time: \(entry.start)")
                     continue
                     continue
@@ -271,21 +303,30 @@ extension Bolus {
                 let entryStartTime = calendar.date(
                 let entryStartTime = calendar.date(
                     bySettingHour: entryComponents.hour!,
                     bySettingHour: entryComponents.hour!,
                     minute: entryComponents.minute!,
                     minute: entryComponents.minute!,
-                    second: entryComponents.second!,
+                    second: entryComponents.second ?? 0, // Set seconds to 0 if not provided
                     of: now
                     of: now
                 )!
                 )!
 
 
                 let entryEndTime: Date
                 let entryEndTime: Date
-                if index < entries.count - 1,
-                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
-                {
-                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
-                    entryEndTime = calendar.date(
-                        bySettingHour: nextEntryComponents.hour!,
-                        minute: nextEntryComponents.minute!,
-                        second: nextEntryComponents.second!,
-                        of: now
-                    )!
+                if index < entries.count - 1 {
+                    // Dynamically set the format again for the next element
+                    if entries[index + 1].start.range(of: regexWithSeconds, options: .regularExpression) != nil {
+                        dateFormatter.dateFormat = "HH:mm:ss"
+                    } else {
+                        dateFormatter.dateFormat = "HH:mm"
+                    }
+
+                    if let nextEntryTime = dateFormatter.date(from: entries[index + 1].start) {
+                        let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                        entryEndTime = calendar.date(
+                            bySettingHour: nextEntryComponents.hour!,
+                            minute: nextEntryComponents.minute!,
+                            second: nextEntryComponents.second ?? 0,
+                            of: now
+                        )!
+                    } else {
+                        entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+                    }
                 } else {
                 } else {
                     entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
                     entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
                 }
                 }
@@ -312,6 +353,7 @@ extension Bolus {
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
         func calculateInsulin() -> Decimal {
+            debug(.bolusState, "calculateInsulin fired")
             let isfForCalculation = isf
             let isfForCalculation = isf
 
 
             // insulin needed for the current blood glucose
             // insulin needed for the current blood glucose
@@ -359,7 +401,6 @@ extension Bolus {
             insulinCalculated = min(insulinCalculated, maxBolus)
             insulinCalculated = min(insulinCalculated, maxBolus)
 
 
             guard let apsManager = apsManager else {
             guard let apsManager = apsManager else {
-                debug(.apsManager, "APSManager could not be gracefully unwrapped")
                 return insulinCalculated
                 return insulinCalculated
             }
             }
 
 
@@ -370,6 +411,7 @@ extension Bolus {
 
 
         func invokeTreatmentsTask() {
         func invokeTreatmentsTask() {
             Task {
             Task {
+                debug(.bolusState, "invokeTreatmentsTask fired")
                 await MainActor.run {
                 await MainActor.run {
                     self.addButtonPressed = true
                     self.addButtonPressed = true
                 }
                 }
@@ -409,6 +451,7 @@ extension Bolus {
         // MARK: - Insulin
         // MARK: - Insulin
 
 
         private func handleInsulin(isExternal: Bool) async throws {
         private func handleInsulin(isExternal: Bool) async throws {
+            debug(.bolusState, "handleInsulin fired")
             if !isExternal {
             if !isExternal {
                 await addPumpInsulin()
                 await addPumpInsulin()
             } else {
             } else {
@@ -557,7 +600,13 @@ extension Bolus {
 
 
 extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
     func determinationDidUpdate(_: Determination) {
     func determinationDidUpdate(_: Determination) {
+        guard isActive else {
+            debug(.bolusState, "skipping determinationDidUpdate; view not active")
+            return
+        }
+
         DispatchQueue.main.async {
         DispatchQueue.main.async {
+            debug(.bolusState, "determinationDidUpdate fired")
             self.waitForSuggestion = false
             self.waitForSuggestion = false
             if self.addButtonPressed {
             if self.addButtonPressed {
                 self.hideModal()
                 self.hideModal()
@@ -567,6 +616,7 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
     func bolusDidFail() {
     func bolusDidFail() {
         DispatchQueue.main.async {
         DispatchQueue.main.async {
+            debug(.bolusState, "bolusDidFail fired")
             self.waitForSuggestion = false
             self.waitForSuggestion = false
             if self.addButtonPressed {
             if self.addButtonPressed {
                 self.hideModal()
                 self.hideModal()
@@ -581,7 +631,8 @@ extension Bolus.StateModel {
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.setupDeterminationsArray()
                 await self.setupDeterminationsArray()
-                await self.updateForecasts()
+                let forecastData = await self.mapForecastsForChart()
+                await self.updateForecasts(with: forecastData)
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
@@ -733,11 +784,18 @@ extension Bolus.StateModel {
 
 
 extension Bolus.StateModel {
 extension Bolus.StateModel {
     @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
     @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
+        guard isActive else {
+            return
+                debug(.bolusState, "updateForecasts not fired")
+        }
+
+        debug(.bolusState, "updateForecasts fired")
         if let forecastData = forecastData {
         if let forecastData = forecastData {
             simulatedDetermination = forecastData
             simulatedDetermination = forecastData
         } else {
         } else {
             simulatedDetermination = await Task { [self] in
             simulatedDetermination = await Task { [self] in
-                await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
+                debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
+                return await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
             }.value
             }.value
         }
         }
 
 

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

@@ -344,10 +344,12 @@ extension Bolus {
             })
             })
             .onAppear {
             .onAppear {
                 configureView {
                 configureView {
+                    state.isActive = true
                     state.insulinCalculated = state.calculateInsulin()
                     state.insulinCalculated = state.calculateInsulin()
                 }
                 }
             }
             }
             .onDisappear {
             .onDisappear {
+                state.isActive = false
                 state.addButtonPressed = false
                 state.addButtonPressed = false
             }
             }
             .sheet(isPresented: $state.showInfo) {
             .sheet(isPresented: $state.showInfo) {

+ 86 - 0
FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift

@@ -9,6 +9,8 @@ struct ForecastChart: View {
 
 
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
 
 
+    @State var selection: Date? = nil
+
     private var endMarker: Date {
     private var endMarker: Date {
         state
         state
             .forecastDisplayType == .lines ? Date(timeIntervalSinceNow: TimeInterval(hours: 3)) :
             .forecastDisplayType == .lines ? Date(timeIntervalSinceNow: TimeInterval(hours: 3)) :
@@ -32,6 +34,12 @@ struct ForecastChart: View {
         return formatter
         return formatter
     }
     }
 
 
+    private var selectedGlucose: GlucoseStored? {
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
+        return state.glucoseFromPersistence.first { $0.date.map(range.contains) ?? false }
+    }
+
     var body: some View {
     var body: some View {
         VStack {
         VStack {
             forecastChartLabels
             forecastChartLabels
@@ -114,7 +122,43 @@ struct ForecastChart: View {
             } else {
             } else {
                 drawForecastsCone()
                 drawForecastsCone()
             }
             }
+
+            if let selectedGlucose {
+                RuleMark(x: .value("Selection", selectedGlucose.date ?? Date.now, unit: .minute))
+                    .foregroundStyle(Color.tabBar)
+                    .lineStyle(.init(lineWidth: 2))
+                    .annotation(
+                        position: .top,
+                        overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
+                    ) {
+                        selectionPopover
+                    }
+
+                PointMark(
+                    x: .value("Time", selectedGlucose.date ?? Date.now, unit: .minute),
+                    y: .value("Value", selectedGlucose.glucose)
+                )
+                .zIndex(-1)
+                .symbolSize(CGSize(width: 15, height: 15))
+                .foregroundStyle(
+                    Decimal(selectedGlucose.glucose) > state.highGlucose ? Color.orange
+                        .opacity(0.8) :
+                        (
+                            Decimal(selectedGlucose.glucose) < state.lowGlucose ? Color.red.opacity(0.8) : Color.green
+                                .opacity(0.8)
+                        )
+                )
+
+                PointMark(
+                    x: .value("Time", selectedGlucose.date ?? Date.now, unit: .minute),
+                    y: .value("Value", selectedGlucose.glucose)
+                )
+                .zIndex(-1)
+                .symbolSize(CGSize(width: 6, height: 6))
+                .foregroundStyle(Color.primary)
+            }
         }
         }
+        .chartXSelection(value: $selection)
         .chartXAxis { forecastChartXAxis }
         .chartXAxis { forecastChartXAxis }
         .chartXScale(domain: startMarker ... endMarker)
         .chartXScale(domain: startMarker ... endMarker)
         .chartYAxis { forecastChartYAxis }
         .chartYAxis { forecastChartYAxis }
@@ -122,6 +166,48 @@ struct ForecastChart: View {
         .backport.chartForegroundStyleScale(state: state)
         .backport.chartForegroundStyleScale(state: state)
     }
     }
 
 
+    @ViewBuilder var selectionPopover: some View {
+        if let sgv = selectedGlucose?.glucose {
+            VStack(alignment: .leading) {
+                HStack {
+                    Image(systemName: "clock")
+                    Text(selectedGlucose?.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
+                        .font(.footnote).bold()
+                }.font(.footnote).padding(.bottom, 5)
+
+                // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                let hardCodedLow = Decimal(55)
+                let hardCodedHigh = Decimal(220)
+                let isDynamicColorScheme = state.glucoseColorScheme == .dynamicColor
+
+                let glucoseColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(sgv),
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : state.highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : state.lowGlucose,
+                    targetGlucose: state.currentBGTarget,
+                    glucoseColorScheme: state.glucoseColorScheme
+                )
+                HStack {
+                    Text(state.units == .mgdL ? Decimal(sgv).description : Decimal(sgv).formattedAsMmolL)
+                        .bold()
+                        + Text(" \(state.units.rawValue)")
+                }.foregroundStyle(
+                    Color(glucoseColor)
+                ).font(.footnote)
+            }
+            .padding(7)
+            .background {
+                RoundedRectangle(cornerRadius: 4)
+                    .fill(Color.chart.opacity(0.85))
+                    .shadow(color: Color.secondary, radius: 2)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 4)
+                            .stroke(Color.secondary, lineWidth: 2)
+                    )
+            }
+        }
+    }
+
     private func drawGlucose() -> some ChartContent {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL

+ 43 - 26
FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -38,10 +38,50 @@ extension CarbRatioEditor {
             return formatter
             return formatter
         }
         }
 
 
-        var body: some View {
-            Form {
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
                 let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
 
 
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Group {
+                    HStack {
+                        HStack {
+                            Button {
+                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                                impactHeavy.impactOccurred()
+                                state.save()
+
+                                // deactivate saving display after 1.25 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                    state.shouldDisplaySaving = false
+                                }
+                            } label: {
+                                HStack {
+                                    if state.shouldDisplaySaving {
+                                        ProgressView().padding(.trailing, 10)
+                                    }
+                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                                }.padding(10)
+                            }
+                        }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                        .disabled(shouldDisableButton)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                        .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                     Section(header: Text("Autotune")) {
                         HStack {
                         HStack {
@@ -56,31 +96,8 @@ extension CarbRatioEditor {
                 Section(header: Text("Schedule")) {
                 Section(header: Text("Schedule")) {
                     list
                     list
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        if state.shouldDisplaySaving {
-                            ProgressView().padding(.trailing, 10)
-                        }
-
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
-
-                            // deactivate saving display after 1.25 seconds
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                state.shouldDisplaySaving = false
-                            }
-                        } label: {
-                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                        }
-                        .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Carb Ratios")
             .navigationTitle("Carb Ratios")

+ 7 - 8
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -20,20 +20,19 @@ extension GlucoseNotificationSettings {
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
                 addSourceInfoToGlucoseNotifications = $0 }
                 addSourceInfoToGlucoseNotifications = $0 }
+
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
-                let value = max(min($0, 400), 40)
-                lowGlucose = value
+                lowGlucose = $0
             }, map: {
             }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
+                let clampedValue = max(min($0, 400), 40)
+                return clampedValue
             })
             })
 
 
             subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
             subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
-                let value = max(min($0, 400), 40)
-                highGlucose = value
+                highGlucose = $0
             }, map: {
             }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
+                let clampedValue = max(min($0, 400), 40)
+                return clampedValue
             })
             })
         }
         }
     }
     }

+ 98 - 31
FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -14,6 +14,8 @@ extension GlucoseNotificationSettings {
         @State var hintLabel: String?
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @State private var booleanPlaceholder: Bool = false
+        @State private var displayPickerLowGlucose: Bool = false
+        @State private var displayPickerHighGlucose: Bool = false
 
 
         private var glucoseFormatter: NumberFormatter {
         private var glucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -127,34 +129,113 @@ extension GlucoseNotificationSettings {
                     verboseHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
                     verboseHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
                 )
                 )
 
 
-                Section {
-                    HStack {
-                        Text("Low Glucose Alarm Limit")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.lowGlucose, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }.padding(.top)
+                self.lowAndHighGlucoseAlertSection
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Glucose Notifications")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
 
 
-                    HStack {
-                        Text("High Glucose Alarm Limit")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.highGlucose, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
+        var lowAndHighGlucoseAlertSection: some View {
+            Section {
+                VStack {
+                    VStack {
+                        HStack {
+                            Text("Low Glucose Alarm Limit")
+
+                            Spacer()
+
+                            Group {
+                                Text(
+                                    state.units == .mgdL ? state.lowGlucose.description : state.lowGlucose.formattedAsMmolL
+                                )
+                                .foregroundColor(!displayPickerLowGlucose ? .primary : .accentColor)
+
+                                Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
+                            }
+                        }
+                        .onTapGesture {
+                            displayPickerLowGlucose.toggle()
+                        }
+                    }
+                    .padding(.top)
+
+                    if displayPickerLowGlucose {
+                        let setting = PickerSettingsProvider.shared.settings.lowGlucose
+
+                        Picker(selection: $state.lowGlucose, label: Text("")) {
+                            ForEach(
+                                PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
+                                id: \.self
+                            ) { value in
+                                let displayValue = state.units == .mgdL ? value.description : value.formattedAsMmolL
+                                Text(displayValue).tag(value)
+                            }
+                        }
+                        .pickerStyle(WheelPickerStyle())
+                        .frame(maxWidth: .infinity)
+                    }
+
+                    VStack {
+                        HStack {
+                            Text("High Glucose Alarm Limit")
+
+                            Spacer()
+
+                            Group {
+                                Text(
+                                    state.units == .mgdL ? state.highGlucose.description : state.highGlucose.formattedAsMmolL
+                                )
+                                .foregroundColor(!displayPickerHighGlucose ? .primary : .accentColor)
+
+                                Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
+                            }
+                        }
+                        .onTapGesture {
+                            displayPickerHighGlucose.toggle()
+                        }
+                    }
+                    .padding(.top)
+
+                    if displayPickerHighGlucose {
+                        let setting = PickerSettingsProvider.shared.settings.highGlucose
+                        Picker(selection: $state.highGlucose, label: Text("")) {
+                            ForEach(
+                                PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
+                                id: \.self
+                            ) { value in
+                                let displayValue = state.units == .mgdL ? value.description : value.formattedAsMmolL
+                                Text(displayValue).tag(value)
+                            }
+                        }
+                        .pickerStyle(WheelPickerStyle())
+                        .frame(maxWidth: .infinity)
                     }
                     }
 
 
                     HStack(alignment: .top) {
                     HStack(alignment: .top) {
                         Text(
                         Text(
                             "Set the lower and upper limit for glucose alarms. See hint for more details."
                             "Set the lower and upper limit for glucose alarms. See hint for more details."
                         )
                         )
+                        .lineLimit(nil)
                         .font(.footnote)
                         .font(.footnote)
                         .foregroundColor(.secondary)
                         .foregroundColor(.secondary)
-                        .lineLimit(nil)
+
                         Spacer()
                         Spacer()
                         Button(
                         Button(
                             action: {
                             action: {
                                 hintLabel = "Low and High Glucose Alarm Limits"
                                 hintLabel = "Low and High Glucose Alarm Limits"
                                 selectedVerboseHint =
                                 selectedVerboseHint =
-                                    "These two settings limit the range outside of which you will be notified via push notifications. If your CGM readings are below 'Low' or above 'High', you will receive a glucose alarm."
+                                    "These two settings limit the range outside of which you will be notified via push notifications. If your CGM readings are below 'Low' or above 'High', you will receive an alarm via push notification."
                                 shouldDisplayHint.toggle()
                                 shouldDisplayHint.toggle()
                             },
                             },
                             label: {
                             label: {
@@ -163,23 +244,9 @@ extension GlucoseNotificationSettings {
                                 }
                                 }
                             }
                             }
                         ).buttonStyle(BorderlessButtonStyle())
                         ).buttonStyle(BorderlessButtonStyle())
-                    }.padding(.vertical)
-                }
-                .listRowBackground(Color.chart)
-            }
-            .sheet(isPresented: $shouldDisplayHint) {
-                SettingInputHintView(
-                    hintDetent: $hintDetent,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    hintLabel: hintLabel ?? "",
-                    hintText: selectedVerboseHint ?? "",
-                    sheetTitle: "Help"
-                )
-            }
-            .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
-            .navigationBarTitle("Glucose Notifications")
-            .navigationBarTitleDisplayMode(.automatic)
+                    }.padding(.top)
+                }.padding(.bottom)
+            }.listRowBackground(Color.chart)
         }
         }
     }
     }
 }
 }

+ 2 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift

@@ -16,7 +16,8 @@ extension Home.StateModel {
             onContext: carbsFetchContext,
             onContext: carbsFetchContext,
             predicate: NSPredicate.carbsForChart,
             predicate: NSPredicate.carbsForChart,
             key: "date",
             key: "date",
-            ascending: false
+            ascending: false,
+            batchSize: 5
         )
         )
 
 
         return await carbsFetchContext.perform {
         return await carbsFetchContext.perform {

+ 2 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -16,7 +16,8 @@ extension Home.StateModel {
             onContext: glucoseFetchContext,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
-            ascending: true
+            ascending: true,
+            batchSize: 50
         )
         )
 
 
         return await glucoseFetchContext.perform {
         return await glucoseFetchContext.perform {

+ 2 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -16,7 +16,8 @@ extension Home.StateModel {
             onContext: pumpHistoryFetchContext,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.pumpHistoryLast24h,
             predicate: NSPredicate.pumpHistoryLast24h,
             key: "timestamp",
             key: "timestamp",
-            ascending: true
+            ascending: true,
+            batchSize: 30
         )
         )
 
 
         return await pumpHistoryFetchContext.perform {
         return await pumpHistoryFetchContext.perform {

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

@@ -68,17 +68,15 @@ extension MainChartView {
                     yStart: .value("rate-start", 0),
                     yStart: .value("rate-start", 0),
                     yEnd: .value("rate-end", basal.rate)
                     yEnd: .value("rate-end", basal.rate)
                 ).foregroundStyle(
                 ).foregroundStyle(
-                    LinearGradient(
-                        gradient: Gradient(
-                            colors: [
-                                Color.insulin.opacity(0.6),
-                                Color.insulin.opacity(0.1)
-                            ]
-                        ),
-                        startPoint: .top,
-                        endPoint: .bottom
+                    .linearGradient(
+                        colors: [
+                            Color.insulin.opacity(0.6),
+                            Color.insulin.opacity(0.1)
+                        ],
+                        startPoint: .bottom,
+                        endPoint: .top
                     )
                     )
-                )
+                ).alignsMarkStylesWithPlotArea()
 
 
                 LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
                 LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
                     .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
                     .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)

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

@@ -42,37 +42,27 @@ struct MainChartView: View {
     }
     }
 
 
     private var selectedGlucose: GlucoseStored? {
     private var selectedGlucose: GlucoseStored? {
-        if let selection = selection {
-            let lowerBound = selection.addingTimeInterval(-150)
-            let upperBound = selection.addingTimeInterval(150)
-            return state.glucoseFromPersistence.first { $0.date ?? now >= lowerBound && $0.date ?? now <= upperBound }
-        } else {
-            return nil
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
+        return state.glucoseFromPersistence.first { $0.date.map(range.contains) ?? false }
+    }
+
+    private func findDetermination(in range: ClosedRange<Date>) -> OrefDetermination? {
+        state.enactedAndNonEnactedDeterminations.first {
+            $0.deliverAt ?? now >= range.lowerBound && $0.deliverAt ?? now <= range.upperBound
         }
         }
     }
     }
 
 
     var selectedCOBValue: OrefDetermination? {
     var selectedCOBValue: OrefDetermination? {
-        if let selection = selection {
-            let lowerBound = selection.addingTimeInterval(-120)
-            let upperBound = selection.addingTimeInterval(120)
-            return state.enactedAndNonEnactedDeterminations.first {
-                $0.deliverAt ?? now >= lowerBound && $0.deliverAt ?? now <= upperBound
-            }
-        } else {
-            return nil
-        }
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-120) ... selection.addingTimeInterval(120)
+        return findDetermination(in: range)
     }
     }
 
 
     var selectedIOBValue: OrefDetermination? {
     var selectedIOBValue: OrefDetermination? {
-        if let selection = selection {
-            let lowerBound = selection.addingTimeInterval(-120)
-            let upperBound = selection.addingTimeInterval(120)
-            return state.enactedAndNonEnactedDeterminations.first {
-                $0.deliverAt ?? now >= lowerBound && $0.deliverAt ?? now <= upperBound
-            }
-        } else {
-            return nil
-        }
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-120) ... selection.addingTimeInterval(120)
+        return findDetermination(in: range)
     }
     }
 
 
     var body: some View {
     var body: some View {

+ 42 - 26
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -39,10 +39,49 @@ extension ISFEditor {
             return formatter
             return formatter
         }
         }
 
 
-        var body: some View {
-            Form {
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.items.isEmpty || !state.hasChanges
                 let shouldDisableButton = state.items.isEmpty || !state.hasChanges
 
 
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Group {
+                    HStack {
+                        HStack {
+                            if state.shouldDisplaySaving {
+                                ProgressView().padding(.trailing, 10)
+                            }
+
+                            Button {
+                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                                impactHeavy.impactOccurred()
+                                state.save()
+
+                                // deactivate saving display after 1.25 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                    state.shouldDisplaySaving = false
+                                }
+                            } label: {
+                                Text(state.shouldDisplaySaving ? "Saving..." : "Save").padding(10)
+                            }
+                        }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                        .disabled(shouldDisableButton)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                        .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                     Section(header: Text("Autotune")) {
                         HStack {
                         HStack {
@@ -99,31 +138,8 @@ extension ISFEditor {
                 Section(header: Text("Schedule")) {
                 Section(header: Text("Schedule")) {
                     list
                     list
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        if state.shouldDisplaySaving {
-                            ProgressView().padding(.trailing, 10)
-                        }
-
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
-
-                            // deactivate saving display after 1.25 seconds
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                state.shouldDisplaySaving = false
-                            }
-                        } label: {
-                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                        }
-                        .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Insulin Sensitivities")
             .navigationTitle("Insulin Sensitivities")

+ 1 - 4
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -28,9 +28,6 @@ extension TrioRemoteControl {
     }
     }
 
 
     private func isRunningInAPNSProductionEnvironment() -> Bool {
     private func isRunningInAPNSProductionEnvironment() -> Bool {
-        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
-            return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
-        }
-        return false
+        return BuildDetails.default.isTestFlightBuild()
     }
     }
 }
 }

+ 13 - 9
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 import Foundation
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
@@ -30,12 +31,12 @@ extension TrioRemoteControl {
     }
     }
 
 
     @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
     @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
-        await disableAllActiveOverrides()
-
         preset.enabled = true
         preset.enabled = true
         preset.date = Date()
         preset.date = Date()
         preset.isUploadedToNS = false
         preset.isUploadedToNS = false
 
 
+        await disableAllActiveOverrides(except: preset.objectID)
+
         do {
         do {
             if viewContext.hasChanges {
             if viewContext.hasChanges {
                 try viewContext.save()
                 try viewContext.save()
@@ -43,17 +44,14 @@ extension TrioRemoteControl {
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 await awaitNotification(.didUpdateOverrideConfiguration)
                 await awaitNotification(.didUpdateOverrideConfiguration)
 
 
-                debug(
-                    .remoteControl,
-                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-                )
+                debug(.remoteControl, "Remote command processed successfully. \(pushMessage.humanReadableDescription())")
             }
             }
         } catch {
         } catch {
             debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
             debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
         }
         }
     }
     }
 
 
-    @MainActor private func disableAllActiveOverrides() async {
+    @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
         let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
         let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
 
         let didPostNotification = await viewContext.perform { () -> Bool in
         let didPostNotification = await viewContext.perform { () -> Bool in
@@ -65,17 +63,23 @@ extension TrioRemoteControl {
                 guard !results.isEmpty else { return false }
                 guard !results.isEmpty else { return false }
 
 
                 for canceledOverride in results where canceledOverride.enabled {
                 for canceledOverride in results where canceledOverride.enabled {
+                    if let overrideID = overrideID, canceledOverride.objectID == overrideID {
+                        continue
+                    }
+
                     let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
                     let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.endDate = Date()
                     newOverrideRunStored.endDate = Date()
-                    newOverrideRunStored
-                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                    newOverrideRunStored.target = NSDecimalNumber(
+                        decimal: self.overrideStorage.calculateTarget(override: canceledOverride)
+                    )
                     newOverrideRunStored.override = canceledOverride
                     newOverrideRunStored.override = canceledOverride
                     newOverrideRunStored.isUploadedToNS = false
                     newOverrideRunStored.isUploadedToNS = false
 
 
                     canceledOverride.enabled = false
                     canceledOverride.enabled = false
+                    canceledOverride.isUploadedToNS = false
                 }
                 }
 
 
                 if self.viewContext.hasChanges {
                 if self.viewContext.hasChanges {

+ 4 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift

@@ -74,6 +74,10 @@ class TrioRemoteControl: Injectable {
             await cancelTempTarget(pushMessage)
             await cancelTempTarget(pushMessage)
         case .meal:
         case .meal:
             await handleMealCommand(pushMessage)
             await handleMealCommand(pushMessage)
+
+            if pushMessage.bolusAmount != nil {
+                await handleBolusCommand(pushMessage)
+            }
         case .startOverride:
         case .startOverride:
             await handleStartOverrideCommand(pushMessage)
             await handleStartOverrideCommand(pushMessage)
         case .cancelOverride:
         case .cancelOverride:

+ 38 - 21
FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -32,38 +32,55 @@ extension TargetsEditor {
             return formatter
             return formatter
         }
         }
 
 
-        var body: some View {
-            Form {
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
                 let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
 
 
-                Section(header: Text("Schedule")) {
-                    list
-                }.listRowBackground(Color.chart)
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
 
 
-                Section {
+                Group {
                     HStack {
                     HStack {
-                        if state.shouldDisplaySaving {
-                            ProgressView().padding(.trailing, 10)
-                        }
-
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
+                        HStack {
+                            Button {
+                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                                impactHeavy.impactOccurred()
+                                state.save()
 
 
-                            // deactivate saving display after 1.25 seconds
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                state.shouldDisplaySaving = false
+                                // deactivate saving display after 1.25 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                    state.shouldDisplaySaving = false
+                                }
+                            } label: {
+                                HStack {
+                                    if state.shouldDisplaySaving {
+                                        ProgressView().padding(.trailing, 10)
+                                    }
+                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                                }.padding(10)
                             }
                             }
-                        } label: {
-                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
                         }
                         }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
                         .disabled(shouldDisableButton)
                         .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
                         .tint(.white)
                         .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
                     }
                     }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
+                Section(header: Text("Schedule")) {
+                    list
+                }.listRowBackground(Color.chart)
             }
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Target Glucose")
             .navigationTitle("Target Glucose")

+ 2 - 2
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -258,8 +258,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
 
 
             let directionText = lastGlucoseObject.directionEnum?.symbol ?? "↔︎"
             let directionText = lastGlucoseObject.directionEnum?.symbol ?? "↔︎"
 
 
-            let deltaValue = settingsManager.settings.units == .mmolL ? Int(delta.asMmolL) : Int(delta)
-            let deltaText = deltaFormatter.string(from: NSNumber(value: deltaValue)) ?? "--"
+            let deltaValue = settingsManager.settings.units == .mmolL ? delta.asMmolL : delta
+            let deltaText = deltaFormatter.string(from: deltaValue as NSNumber) ?? "--"
 
 
             let iobText = iobFormatter.string(from: (determinationObject.iob ?? 0) as NSNumber) ?? ""
             let iobText = iobFormatter.string(from: (determinationObject.iob ?? 0) as NSNumber) ?? ""
             let cobText = cobFormatter.string(from: determinationObject.cob as NSNumber) ?? ""
             let cobText = cobFormatter.string(from: determinationObject.cob as NSNumber) ?? ""

+ 3 - 2
FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -33,7 +33,7 @@ extension LiveActivityBridge {
             key: "deliverAt",
             key: "deliverAt",
             ascending: false,
             ascending: false,
             fetchLimit: 1,
             fetchLimit: 1,
-            propertiesToFetch: ["iob", "cob", "currentTarget"]
+            propertiesToFetch: ["iob", "cob", "currentTarget", "deliverAt"]
         )
         )
 
 
         return await context.perform {
         return await context.perform {
@@ -45,7 +45,8 @@ extension LiveActivityBridge {
                 DeterminationData(
                 DeterminationData(
                     cob: ($0["cob"] as? Int) ?? 0,
                     cob: ($0["cob"] as? Int) ?? 0,
                     iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0,
                     iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0,
-                    target: ($0["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0
+                    target: ($0["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0,
+                    date: $0["deliverAt"] as? Date ?? nil
                 )
                 )
             }
             }
         }
         }

+ 1 - 0
FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift

@@ -4,4 +4,5 @@ struct DeterminationData {
     let cob: Int
     let cob: Int
     let iob: Decimal
     let iob: Decimal
     let target: Decimal
     let target: Decimal
+    let date: Date?
 }
 }

+ 1 - 1
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -16,7 +16,7 @@ struct LiveActivityAttributes: ActivityAttributes {
         let bg: String
         let bg: String
         let direction: String?
         let direction: String?
         let change: String
         let change: String
-        let date: Date
+        let date: Date?
         let highGlucose: Decimal
         let highGlucose: Decimal
         let lowGlucose: Decimal
         let lowGlucose: Decimal
         let target: Decimal
         let target: Decimal

+ 1 - 1
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -126,7 +126,7 @@ extension LiveActivityAttributes.ContentState {
             bg: formattedBG,
             bg: formattedBG,
             direction: trendString,
             direction: trendString,
             change: change,
             change: change,
-            date: bg.date,
+            date: determination?.date ?? nil,
             highGlucose: settings.high,
             highGlucose: settings.high,
             lowGlucose: settings.low,
             lowGlucose: settings.low,
             target: determination?.target ?? 100 as Decimal,
             target: determination?.target ?? 100 as Decimal,

+ 29 - 46
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -15,18 +15,17 @@ import UIKit
              .ended,
              .ended,
              .stale:
              .stale:
             return true
             return true
-        case .active: break
+        case .active:
+            break
         @unknown default:
         @unknown default:
             return true
             return true
         }
         }
-
-        return -startDate.timeIntervalSinceNow >
-            TimeInterval(60 * 60)
+        return -startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
     }
     }
 }
 }
 
 
-@available(iOS 16.2, *) final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver
-{
+@available(iOS 16.2, *)
+final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
@@ -90,8 +89,6 @@ import UIKit
         )
         )
     }
     }
 
 
-    // TODO: - use a delegate or a custom notification here instead
-
     func settingsDidChange(_: FreeAPSSettings) {
     func settingsDidChange(_: FreeAPSSettings) {
         Task {
         Task {
             await updateContentState(determination)
             await updateContentState(determination)
@@ -99,7 +96,6 @@ import UIKit
     }
     }
 
 
     private func registerHandler() {
     private func registerHandler() {
-        // Since we are only using this info to show if an Override is active or not in the Live Activity it is enough to observe only the 'OverrideStored' Entity
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.overridesDidUpdate()
             self.overridesDidUpdate()
@@ -141,15 +137,16 @@ import UIKit
 
 
     @objc private func handleLiveActivityOrderChange() {
     @objc private func handleLiveActivityOrderChange() {
         Task {
         Task {
-            self.widgetItems = UserDefaults.standard
-                .loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes.LiveActivityItem.defaultItems
+            self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
+                .LiveActivityItem.defaultItems
             await self.updateLiveActivityOrder()
             await self.updateLiveActivityOrder()
         }
         }
     }
     }
 
 
     @MainActor private func updateContentState<T>(_ update: T) async {
     @MainActor private func updateContentState<T>(_ update: T) async {
-        guard let latestGlucose = latestGlucose else { return }
-
+        guard let latestGlucose = latestGlucose else {
+            return
+        }
         var content: LiveActivityAttributes.ContentState?
         var content: LiveActivityAttributes.ContentState?
 
 
         if let determination = update as? DeterminationData {
         if let determination = update as? DeterminationData {
@@ -189,10 +186,7 @@ import UIKit
 
 
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task { @MainActor in
         Task { @MainActor in
-            // Fetch and map glucose to GlucoseData struct
             self.glucoseFromPersistence = await fetchAndMapGlucose()
             self.glucoseFromPersistence = await fetchAndMapGlucose()
-
-            // Push the update to the Live Activity
             glucoseDidUpdate(glucoseFromPersistence ?? [])
             glucoseDidUpdate(glucoseFromPersistence ?? [])
         }
         }
     }
     }
@@ -209,14 +203,9 @@ 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
     @MainActor 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 {
         if settings.useLiveActivity {
-            if currentActivity?.needsRecreation() ?? true
-            {
+            if currentActivity?.needsRecreation() ?? true {
                 glucoseDidUpdate(glucoseFromPersistence ?? [])
                 glucoseDidUpdate(glucoseFromPersistence ?? [])
             }
             }
         } else {
         } else {
@@ -226,9 +215,7 @@ import UIKit
         }
         }
     }
     }
 
 
-    /// attempts to present this live activity state, creating a new activity if none exists yet
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
-//        // End all activities that are not the current one
         for unknownActivity in Activity<LiveActivityAttributes>.activities
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
             .filter({ self.currentActivity?.activity.id != $0.id })
         {
         {
@@ -242,32 +229,29 @@ import UIKit
             } else {
             } else {
                 let content = ActivityContent(
                 let content = ActivityContent(
                     state: state,
                     state: state,
-                    staleDate: min(state.date, Date.now).addingTimeInterval(360) // 6 minutes in seconds
+                    staleDate: min(state.date ?? Date.now, Date.now).addingTimeInterval(360) // 6 minutes in seconds
                 )
                 )
                 await currentActivity.activity.update(content)
                 await currentActivity.activity.update(content)
             }
             }
         } else {
         } else {
             do {
             do {
-                // always push a non-stale content as the first update
-                // pushing a stale content as the frst content results in the activity not being shown at all
-                // apparently this initial state is also what is shown after the live activity expires (after 8h)
                 let expired = ActivityContent(
                 let expired = ActivityContent(
-                    state: LiveActivityAttributes.ContentState(
-                        bg: "--",
-                        direction: nil,
-                        change: "--",
-                        date: Date.now,
-                        highGlucose: settings.high,
-                        lowGlucose: settings.low,
-                        target: determination?.target ?? 100 as Decimal,
-                        glucoseColorScheme: settings.glucoseColorScheme.rawValue,
-                        detailedViewState: nil,
-                        isInitialState: true
-                    ),
+                    state: LiveActivityAttributes
+                        .ContentState(
+                            bg: "--",
+                            direction: nil,
+                            change: "--",
+                            date: Date.now,
+                            highGlucose: settings.high,
+                            lowGlucose: settings.low,
+                            target: determination?.target ?? 100 as Decimal,
+                            glucoseColorScheme: settings.glucoseColorScheme.rawValue,
+                            detailedViewState: nil,
+                            isInitialState: true
+                        ),
                     staleDate: Date.now.addingTimeInterval(60)
                     staleDate: Date.now.addingTimeInterval(60)
                 )
                 )
 
 
-                // Request a new activity
                 let activity = try Activity.request(
                 let activity = try Activity.request(
                     attributes: LiveActivityAttributes(startDate: Date.now),
                     attributes: LiveActivityAttributes(startDate: Date.now),
                     content: expired,
                     content: expired,
@@ -275,22 +259,22 @@ import UIKit
                 )
                 )
                 currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
                 currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
 
 
-                // then show the actual content
                 await pushUpdate(state)
                 await pushUpdate(state)
             } catch {
             } catch {
-                print("Activity creation error: \(error)")
+                debug(
+                    .default,
+                    "\(#file): Error creating new activity: \(error)"
+                )
             }
             }
         }
         }
     }
     }
 
 
-    /// ends all live activities immediateny
     private func endActivity() async {
     private func endActivity() async {
         if let currentActivity {
         if let currentActivity {
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             self.currentActivity = nil
             self.currentActivity = nil
         }
         }
 
 
-        // end any other activities
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
         }
@@ -309,7 +293,6 @@ extension LiveActivityBridge {
             return
             return
         }
         }
 
 
-        // backfill latest glucose if contained in this update
         if glucose.count > 1 {
         if glucose.count > 1 {
             latestGlucose = glucose.dropFirst().first
             latestGlucose = glucose.dropFirst().first
         }
         }

+ 8 - 4
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -15,6 +15,10 @@ protocol NightscoutManager: GlucoseSource {
     func deleteManualGlucose(withID id: String) async
     func deleteManualGlucose(withID id: String) async
     func uploadStatus() async
     func uploadStatus() async
     func uploadGlucose() async
     func uploadGlucose() async
+    func uploadCarbs() async
+    func uploadPumpHistory() async
+    func uploadOverrides() async
+    func uploadTempTargets() async
     func uploadManualGlucose() async
     func uploadManualGlucose() async
     func uploadProfiles() async
     func uploadProfiles() async
     func importSettings() async -> ScheduledNightscoutProfile?
     func importSettings() async -> ScheduledNightscoutProfile?
@@ -692,24 +696,24 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
         await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
     }
     }
 
 
-    private func uploadPumpHistory() async {
+    func uploadPumpHistory() async {
         await uploadTreatments(
         await uploadTreatments(
             pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout(),
             pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout(),
             fileToSave: OpenAPS.Nightscout.uploadedPumphistory
             fileToSave: OpenAPS.Nightscout.uploadedPumphistory
         )
         )
     }
     }
 
 
-    private func uploadCarbs() async {
+    func uploadCarbs() async {
         await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
         await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
         await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
         await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
     }
     }
 
 
-    private func uploadOverrides() async {
+    func uploadOverrides() async {
         await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
         await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
         await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
         await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
     }
     }
 
 
-    private func uploadTempTargets() async {
+    func uploadTempTargets() async {
         await uploadTreatments(
         await uploadTreatments(
             tempTargetsStorage.nightscoutTreatmentsNotUploaded(),
             tempTargetsStorage.nightscoutTreatmentsNotUploaded(),
             fileToSave: OpenAPS.Nightscout.uploadedTempTargets
             fileToSave: OpenAPS.Nightscout.uploadedTempTargets

+ 21 - 17
Gemfile.lock

@@ -10,20 +10,20 @@ GEM
     artifactory (3.0.17)
     artifactory (3.0.17)
     atomos (0.1.3)
     atomos (0.1.3)
     aws-eventstream (1.3.0)
     aws-eventstream (1.3.0)
-    aws-partitions (1.981.0)
-    aws-sdk-core (3.209.1)
+    aws-partitions (1.1007.0)
+    aws-sdk-core (3.213.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-eventstream (~> 1, >= 1.3.0)
-      aws-partitions (~> 1, >= 1.651.0)
+      aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
       aws-sigv4 (~> 1.9)
       jmespath (~> 1, >= 1.6.1)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.94.0)
-      aws-sdk-core (~> 3, >= 3.207.0)
+    aws-sdk-kms (1.95.0)
+      aws-sdk-core (~> 3, >= 3.210.0)
       aws-sigv4 (~> 1.5)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.166.0)
-      aws-sdk-core (~> 3, >= 3.207.0)
+    aws-sdk-s3 (1.171.0)
+      aws-sdk-core (~> 3, >= 3.210.0)
       aws-sdk-kms (~> 1)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
       aws-sigv4 (~> 1.5)
-    aws-sigv4 (1.10.0)
+    aws-sigv4 (1.10.1)
       aws-eventstream (~> 1, >= 1.0.2)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
     babosa (1.0.4)
     base64 (0.2.0)
     base64 (0.2.0)
@@ -69,7 +69,7 @@ GEM
     faraday_middleware (1.2.1)
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
       faraday (~> 1.0)
     fastimage (2.3.1)
     fastimage (2.3.1)
-    fastlane (2.223.1)
+    fastlane (2.225.0)
       CFPropertyList (>= 2.3, < 4.0.0)
       CFPropertyList (>= 2.3, < 4.0.0)
       addressable (>= 2.8, < 3.0.0)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
       artifactory (~> 3.0)
@@ -85,6 +85,7 @@ GEM
       faraday-cookie_jar (~> 0.0.6)
       faraday-cookie_jar (~> 0.0.6)
       faraday_middleware (~> 1.0)
       faraday_middleware (~> 1.0)
       fastimage (>= 2.1.0, < 3.0.0)
       fastimage (>= 2.1.0, < 3.0.0)
+      fastlane-sirp (>= 1.0.0)
       gh_inspector (>= 1.1.2, < 2.0.0)
       gh_inspector (>= 1.1.2, < 2.0.0)
       google-apis-androidpublisher_v3 (~> 0.3)
       google-apis-androidpublisher_v3 (~> 0.3)
       google-apis-playcustomapp_v1 (~> 0.1)
       google-apis-playcustomapp_v1 (~> 0.1)
@@ -110,6 +111,8 @@ GEM
       xcodeproj (>= 1.13.0, < 2.0.0)
       xcodeproj (>= 1.13.0, < 2.0.0)
       xcpretty (~> 0.3.0)
       xcpretty (~> 0.3.0)
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+    fastlane-sirp (1.0.0)
+      sysrandom (~> 1.0)
     gh_inspector (1.1.3)
     gh_inspector (1.1.3)
     google-apis-androidpublisher_v3 (0.54.0)
     google-apis-androidpublisher_v3 (0.54.0)
       google-apis-core (>= 0.11.0, < 2.a)
       google-apis-core (>= 0.11.0, < 2.a)
@@ -152,17 +155,17 @@ GEM
       domain_name (~> 0.5)
       domain_name (~> 0.5)
     httpclient (2.8.3)
     httpclient (2.8.3)
     jmespath (1.6.2)
     jmespath (1.6.2)
-    json (2.7.2)
-    jwt (2.9.1)
+    json (2.7.6)
+    jwt (2.9.3)
       base64
       base64
     mini_magick (4.13.2)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
     mini_mime (1.1.5)
     multi_json (1.15.0)
     multi_json (1.15.0)
     multipart-post (2.4.1)
     multipart-post (2.4.1)
-    nanaimo (0.3.0)
+    nanaimo (0.4.0)
     naturally (2.2.1)
     naturally (2.2.1)
     nkf (0.2.0)
     nkf (0.2.0)
-    optparse (0.5.0)
+    optparse (0.6.0)
     os (1.1.4)
     os (1.1.4)
     plist (3.7.1)
     plist (3.7.1)
     public_suffix (5.1.1)
     public_suffix (5.1.1)
@@ -172,7 +175,7 @@ GEM
       trailblazer-option (>= 0.1.1, < 0.2.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
       uber (< 0.2.0)
     retriable (3.1.2)
     retriable (3.1.2)
-    rexml (3.3.7)
+    rexml (3.3.9)
     rouge (2.0.7)
     rouge (2.0.7)
     ruby2_keywords (0.0.5)
     ruby2_keywords (0.0.5)
     rubyzip (2.3.2)
     rubyzip (2.3.2)
@@ -185,6 +188,7 @@ GEM
     simctl (1.6.10)
     simctl (1.6.10)
       CFPropertyList
       CFPropertyList
       naturally
       naturally
+    sysrandom (1.0.5)
     terminal-notifier (2.0.0)
     terminal-notifier (2.0.0)
     terminal-table (3.0.2)
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
       unicode-display_width (>= 1.1.1, < 3)
@@ -197,13 +201,13 @@ GEM
     unf (0.2.0)
     unf (0.2.0)
     unicode-display_width (2.6.0)
     unicode-display_width (2.6.0)
     word_wrap (1.0.0)
     word_wrap (1.0.0)
-    xcodeproj (1.25.0)
+    xcodeproj (1.27.0)
       CFPropertyList (>= 2.3.3, < 4.0)
       CFPropertyList (>= 2.3.3, < 4.0)
       atomos (~> 0.1.3)
       atomos (~> 0.1.3)
       claide (>= 1.0.2, < 2.0)
       claide (>= 1.0.2, < 2.0)
       colored2 (~> 3.1)
       colored2 (~> 3.1)
-      nanaimo (~> 0.3.0)
-      rexml (>= 3.3.2, < 4.0)
+      nanaimo (~> 0.4.0)
+      rexml (>= 3.3.6, < 4.0)
     xcpretty (0.3.0)
     xcpretty (0.3.0)
       rouge (~> 2.0.7)
       rouge (~> 2.0.7)
     xcpretty-travis-formatter (1.0.1)
     xcpretty-travis-formatter (1.0.1)

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift

@@ -20,7 +20,7 @@ struct LiveActivityUpdatedLabelView: View {
     }
     }
 
 
     var body: some View {
     var body: some View {
-        let dateText = Text("\(dateFormatter.string(from: context.state.date))")
+        let dateText = Text("\((context.state.date != nil) ? dateFormatter.string(from: context.state.date!) : "--")")
 
 
         if isDetailedLayout {
         if isDetailedLayout {
             VStack {
             VStack {

+ 0 - 1
Model/CoreDataStack.swift

@@ -18,7 +18,6 @@ class CoreDataStack: ObservableObject {
             object: nil,
             object: nil,
             queue: nil
             queue: nil
         ) { _ in
         ) { _ in
-            debugPrint("Received a persistent store remote change notification")
             Task {
             Task {
                 await self.fetchPersistentHistory()
                 await self.fetchPersistentHistory()
             }
             }