Browse Source

CGM UI coherence with pump UI

Align the CGM setup view with the pump setup view :
- A UI for all CGM with a “add” button and menu choice
- A new view for CGM without specific config view (plugin)
- fix deletion CGM action and probably resolve issues about not remove CGM
- fix the calibration function with new coredata.
avouspierre 1 year ago
parent
commit
7a02890a1d

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -368,6 +368,7 @@
 		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
 		CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidepoolConfigView.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
+		CE3EEF9A2D463717001944DD /* OtherCGMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3EEF992D46370A001944DD /* OtherCGMView.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
 		CE51DD1C2A01970900F163F7 /* ConnectIQ 2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */; };
@@ -1078,6 +1079,7 @@
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D17297C9EE800DF218F /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE398D1A297D69A900DF218F /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		CE3EEF992D46370A001944DD /* OtherCGMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCGMView.swift; sourceTree = "<group>"; };
 		CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBLEPumpManagerExtensions.swift; sourceTree = "<group>"; };
 		CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniPodManagerExtensions.swift; sourceTree = "<group>"; };
 		CE51DD1B2A01970800F163F7 /* ConnectIQ 2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "ConnectIQ 2.xcframework"; path = "Dependencies/ConnectIQ 2.xcframework"; sourceTree = "<group>"; };
@@ -1355,6 +1357,7 @@
 		0D76BBC81CEDC1A0050F45EF /* View */ = {
 			isa = PBXGroup;
 			children = (
+				CE3EEF992D46370A001944DD /* OtherCGMView.swift */,
 				38569352270B5E350002C50D /* CGMRootView.swift */,
 				CE7950232997D81700FA576E /* CGMSettingsView.swift */,
 				CE7950252998056D00FA576E /* CGMSetupView.swift */,
@@ -3655,6 +3658,7 @@
 				DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */,
 				DD2CC85C2D25DA1000445446 /* GlucoseTargetsView.swift in Sources */,
 				190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */,
+				CE3EEF9A2D463717001944DD /* OtherCGMView.swift in Sources */,
 				5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,

+ 1 - 1
FreeAPS/Sources/APS/CGM/PluginSource.swift

@@ -108,7 +108,7 @@ extension PluginSource: CGMManagerDelegate {
         dispatchPrecondition(condition: .onQueue(processQueue))
         debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
         // TODO:
-        glucoseManager?.cgmGlucoseSourceType = .none
+        glucoseManager?.deleteGlucoseSource()
     }
 
     func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {

+ 1 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -129,6 +129,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         if let manager = newManager
         {
             cgmManager = manager
+            glucoseSource = nil
             removeCalibrations()
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)

+ 23 - 23
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -18,6 +18,10 @@ let cgmDefaultName = cgmName(
     subtitle: CGMType.none.subtitle
 )
 
+struct EmptyCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var cgmManager: FetchGlucoseManager!
@@ -95,28 +99,6 @@ extension CGM {
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
 
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
-
-            $cgmCurrent
-                .removeDuplicates()
-                .sink { [weak self] value in
-                    guard let self = self else { return }
-                    guard self.cgmManager.cgmGlucoseSourceType != nil else {
-                        self.settingsManager.settings.cgm = .none
-                        return
-                    }
-                    if value.type != self.settingsManager.settings.cgm ||
-                        value.id != self.settingsManager.settings.cgmPluginIdentifier
-                    {
-                        self.settingsManager.settings.cgm = value.type
-                        self.settingsManager.settings.cgmPluginIdentifier = value.id
-                        self.cgmManager.updateGlucoseSource(
-                            cgmGlucoseSourceType: value.type,
-                            cgmGlucosePluginId: value.id
-                        )
-                        self.setupCGM = false
-                    }
-                }
-                .store(in: &lifetime)
         }
 
         func displayNameOfApp() -> String? {
@@ -140,6 +122,22 @@ extension CGM {
                 return cgmManager.cgmGlucoseSourceType.appURL
             }
         }
+
+        func addCGM(cgm: cgmName) {
+            cgmCurrent = cgm
+            switch cgmCurrent.type {
+            case .plugin:
+                setupCGM.toggle()
+            default:
+                cgmManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(EmptyCompletionNotifying())
+            }
+        }
+
+        func deleteCGM() {
+            cgmManager.deleteGlucoseSource()
+            completionNotifyingDidComplete(EmptyCompletionNotifying())
+        }
     }
 }
 
@@ -148,12 +146,14 @@ extension CGM.StateModel: CompletionDelegate {
         setupCGM = false
 
         // if CGM was deleted
-        if cgmManager.cgmGlucoseSourceType == nil {
+        if cgmManager.cgmGlucoseSourceType == .none {
             cgmCurrent = cgmDefaultName
             settingsManager.settings.cgm = cgmDefaultName.type
             settingsManager.settings.cgmPluginIdentifier = cgmDefaultName.id
             cgmManager.deleteGlucoseSource()
         } else {
+            settingsManager.settings.cgm = cgmCurrent.type
+            settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
             cgmManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
         }
 

+ 145 - 177
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -7,7 +7,6 @@ extension CGM {
         let resolver: Resolver
         let displayClose: Bool
         @StateObject var state = StateModel()
-        @State private var setupCGM = false
 
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
@@ -15,174 +14,71 @@ extension CGM {
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
+        @State var showCGMSelection: Bool = false
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
         var body: some View {
             NavigationView {
-                List {
+                Form {
                     Section(
                         header: Text("CGM Integration to Trio"),
                         content: {
-                            VStack {
-                                Picker("Type", selection: $state.cgmCurrent) {
-                                    ForEach(state.listOfCGM) { type in
-                                        VStack(alignment: .leading) {
-                                            Text(type.displayName)
-                                            Text(type.subtitle).font(.caption).foregroundColor(.secondary)
-                                        }.tag(type)
-                                    }
-                                }.padding(.top)
-
-                                HStack(alignment: .center) {
-                                    Text(
-                                        "Select your CGM. See hint for compatible devices."
-                                    )
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
-                                    .lineLimit(nil)
-                                    Spacer()
-                                    Button(
-                                        action: {
-                                            hintLabel = "Available CGM Types for Trio"
-                                            selectedVerboseHint =
-                                                AnyView(
-                                                    Text(
-                                                        "• Dexcom G5 \n• Dexcom G6 / ONE \n• Dexcom G7 / ONE+ \n• Dexcom Share \n• Freestyle Libre \n• Freestyle Libre Demo \n• Glucose Simulator \n• Medtronic Enlite \n• Nightscout \n• xDrip4iOS"
-                                                    )
-                                                )
-                                            shouldDisplayHint.toggle()
-                                        },
-                                        label: {
-                                            HStack {
-                                                Image(systemName: "questionmark.circle")
-                                            }
-                                        }
-                                    ).buttonStyle(BorderlessButtonStyle())
-                                }.padding(.top)
-                            }.padding(.bottom)
-
-                            if let link = state.cgmCurrent.type.externalLink {
+                            let cgmState = state.cgmCurrent
+                            if cgmState.type != .none {
                                 Button {
-                                    UIApplication.shared.open(link, options: [:], completionHandler: nil)
-                                } label: {
-                                    HStack {
-                                        Text("About this source")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
-                                    }
-                                }
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                            }
+                                    state.setupCGM = true
 
-                            if state.cgmCurrent.type == .plugin {
-                                Button {
-                                    setupCGM.toggle()
                                 } label: {
                                     HStack {
-                                        Text("CGM Configuration")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
+                                        Image(systemName: "sensor.tag.radiowaves.forward.fill").padding()
+                                        Text(cgmState.displayName)
                                     }
                                 }
-                                .frame(maxWidth: .infinity, alignment: .leading)
-                            }
-                        }
-                    ).listRowBackground(Color.chart)
 
-                    if let appURL = state.urlOfApp() {
-                        Section {
-                            Button {
-                                UIApplication.shared.open(appURL, options: [:]) { success in
-                                    if !success {
-                                        self.router.alertMessage
-                                            .send(MessageContent(content: "Unable to open the app", type: .warning))
-                                    }
-                                }
-                            }
-
-                            label: {
-                                Label(state.displayNameOfApp() ?? "-", systemImage: "waveform.path.ecg.rectangle").font(.title3)
-                                    .padding() }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                        }
-                        .listRowBackground(Color.clear)
-                    } else if state.cgmCurrent.type == .nightscout {
-                        if let url = state.url {
-                            Section {
-                                Button {
-                                    UIApplication.shared.open(url, options: [:]) { success in
-                                        if !success {
-                                            self.router.alertMessage
-                                                .send(MessageContent(content: "No URL available", type: .warning))
-                                        }
-                                    }
-                                }
-                                label: { Label("Open URL", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding() }
-                                    .frame(maxWidth: .infinity, alignment: .center)
-                                    .buttonStyle(.bordered)
-                            }
-                            .listRowBackground(Color.clear)
-                        } else {
-                            Section {
-                                Button {
-                                    state.showModal(for: .nighscoutConfigDirect)
-                                }
-                                label: {
-                                    Label("Config Nightscout", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding()
-                                }
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .buttonStyle(.bordered)
-                            }
-                            .listRowBackground(Color.clear)
-                        }
-                    }
+                            } else {
+                                VStack {
+                                    Button {
+                                        showCGMSelection.toggle()
+                                    } label: {
+                                        Text("Add CGM")
+                                            .font(.title3) }
+                                        .frame(maxWidth: .infinity, alignment: .center)
+                                        .buttonStyle(.bordered)
 
-                    if state.cgmCurrent.type == .xdrip {
-                        Section(header: Text("Heartbeat")) {
-                            VStack(alignment: .leading) {
-                                if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
-                                    Text("CGM address :").padding(.top)
-                                    Text(cgmTransmitterDeviceAddress)
-                                } else {
-                                    Text("CGM is not used as heartbeat.").padding(.top)
-                                }
-
-                                HStack(alignment: .center) {
-                                    Text(
-                                        "A heartbeat tells Trio to start a loop cycle. This is required for closed loop."
-                                    )
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
-                                    .lineLimit(nil)
-                                    Spacer()
-                                    Button(
-                                        action: {
-                                            hintLabel = "CGM Heartbeat"
-                                            selectedVerboseHint =
-                                                AnyView(
-                                                    Text(
-                                                        "The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
-                                                    )
-                                                )
-                                            shouldDisplayHint.toggle()
-                                        },
-                                        label: {
-                                            HStack {
-                                                Image(systemName: "questionmark.circle")
+                                    HStack(alignment: .center) {
+                                        Text(
+                                            "Pair your CGM with Trio. See hint for compatible devices."
+                                        )
+                                        .font(.footnote)
+                                        .foregroundColor(.secondary)
+                                        .lineLimit(nil)
+                                        Spacer()
+                                        Button(
+                                            action: {
+                                                shouldDisplayHint.toggle()
+                                            },
+                                            label: {
+                                                HStack {
+                                                    Image(systemName: "questionmark.circle")
+                                                }
                                             }
-                                        }
-                                    ).buttonStyle(BorderlessButtonStyle())
+                                        ).buttonStyle(BorderlessButtonStyle())
+                                    }.padding(.top)
                                 }.padding(.vertical)
                             }
-                        }.listRowBackground(Color.chart)
-                    }
+                        }
+                    )
+                    .padding(.top)
+                    .listRowBackground(Color.chart)
 
                     if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
                         Section {
-                            Text("Libre Calibrations").navigationLink(to: .calibrations, from: self)
+                            NavigationLink(
+                                destination: Calibrations.RootView(resolver: resolver),
+                                label: { Text("Libre Calibrations") }
+                            )
                         }.listRowBackground(Color.chart)
                     }
 
@@ -221,45 +117,117 @@ extension CGM {
                 .navigationTitle("CGM")
                 .navigationBarTitleDisplayMode(.automatic)
                 .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
+                .sheet(isPresented: $state.setupCGM) {
+                    switch state.cgmCurrent.type {
+                    case .enlite,
+                         .nightscout,
+                         .none,
+                         .simulator,
+                         .xdrip:
+
+                        OtherCGMView(resolver: self.resolver, state: state)
+
+                    case .plugin:
+                        if let cgmFetchManager = state.cgmManager,
+                           let cgmManager = cgmFetchManager.cgmManager,
+                           state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
+                           state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
+                        {
+                            CGMSettingsView(
+                                cgmManager: cgmManager,
+                                bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                                unit: state.settingsManager.settings.units,
+                                completionDelegate: state
+                            )
+                        } else {
+                            CGMSetupView(
+                                CGMType: state.cgmCurrent,
+                                bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                                unit: state.settingsManager.settings.units,
+                                completionDelegate: state,
+                                setupDelegate: state,
+                                pluginCGMManager: self.state.pluginCGMManager
+                            )
+                        }
+                    }
+                }
                 .sheet(isPresented: $shouldDisplayHint) {
                     SettingInputHintView(
                         hintDetent: $hintDetent,
                         shouldDisplayHint: $shouldDisplayHint,
                         hintLabel: hintLabel ?? "",
-                        hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                        hintText: AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Current CGM Models Supported:"
+                                )
+                                VStack(alignment: .leading) {
+                                    Text("• Dexcom G5")
+                                    Text("• Dexcom G6 / ONE")
+                                    Text("• Dexcom G7 / ONE+")
+                                    Text("• Dexcom Share")
+                                    Text("• Freestyle Libre")
+                                    Text("• Freestyle Libre Demo")
+                                    Text("• Glucose Simulator")
+                                    Text("• Medtronic Enlite")
+                                    Text("• Nightscout")
+                                    Text("• xDrip4iOS")
+                                }
+                                Text(
+                                    "Note: The CGM Heartbeat can come from either a CGM or a pump to wake up Trio when phone is locked or in the background. If CGM is on the same phone as Trio and xDrip4iOS is configured to use the same AppGroup as Trio and the heartbeat feature is turned on in xDrip4iOS, then the CGM can provide a heartbeat to wake up Trio when phone is locked or app is in the background."
+                                )
+                            }
+                        ),
                         sheetTitle: "Help"
                     )
                 }
-                .onChange(of: setupCGM) { _, setupCGM in
-                    state.setupCGM = setupCGM
-                }
-                .onChange(of: state.setupCGM) { _, setupCGM in
-                    self.setupCGM = setupCGM
-                }
-                .screenNavigation(self)
-            }
-            .sheet(isPresented: $setupCGM) {
-                if let cgmFetchManager = state.cgmManager,
-                   let cgmManager = cgmFetchManager.cgmManager,
-                   state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
-                   state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
-                {
-                    CGMSettingsView(
-                        cgmManager: cgmManager,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        unit: state.settingsManager.settings.units,
-                        completionDelegate: state
-                    )
-                } else {
-                    CGMSetupView(
-                        CGMType: state.cgmCurrent,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        unit: state.settingsManager.settings.units,
-                        completionDelegate: state,
-                        setupDelegate: state,
-                        pluginCGMManager: self.state.pluginCGMManager
-                    )
-                }
+                .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
+                    Button("Nightscout") { state.addCGM(cgm: state.listOfCGM.first(where: { $0.type == .nightscout })!) }
+                    Button("Dexcom G5") {
+                        state.addCGM(cgm: state.listOfCGM.first(where: { $0.type == .plugin && $0.displayName.contains("G5") })!)
+                    }
+                    Button("Dexcom G6 / ONE") {
+                        state
+                            .addCGM(
+                                cgm: state.listOfCGM
+                                    .first(where: { $0.type == .plugin && $0.displayName.contains("G6") })!
+                            )
+                    }
+                    Button("Dexcom G7 / ONE+") {
+                        state
+                            .addCGM(
+                                cgm: state.listOfCGM
+                                    .first(where: { $0.type == .plugin && $0.displayName.contains("G7") })!
+                            )
+                    }
+                    Button("Dexcom Share") {
+                        state.addCGM(
+                            cgm: state.listOfCGM
+                                .first(where: { $0.type == .plugin && $0.displayName.contains("Dexcom Share") })!
+                        ) }
+                    Button("FreeStyle Libre") {
+                        state.addCGM(
+                            cgm: state.listOfCGM
+                                .first(
+                                    where: { $0.type == .plugin && $0.displayName == "FreeStyle Libre" }
+                                )!
+                        ) }
+                    Button("FreeStyle Libre Demo") {
+                        state.addCGM(
+                            cgm: state.listOfCGM
+                                .first(where: { $0.type == .plugin && $0.displayName == "FreeStyle Libre Demo" })!
+                        ) }
+                    Button("Medtronic Enlite") {
+                        state.addCGM(cgm: state.listOfCGM.first(where: { $0.type == .enlite })!) }
+                    Button("xDrip4iOS") {
+                        state.addCGM(cgm: state.listOfCGM.first(where: { $0.type == .xdrip })!) }
+                    Button("Glucose Simulator") {
+                        state
+                            .addCGM(
+                                cgm: state.listOfCGM
+                                    .first(where: { $0.type == .simulator })!
+                            ) }
+                } message: { Text("Select CGM Model") }
             }
         }
     }

+ 97 - 0
FreeAPS/Sources/Modules/CGM/View/OtherCGMView.swift

@@ -0,0 +1,97 @@
+import LoopKitUI
+import SwiftUI
+import Swinject
+
+struct OtherCGMView: BaseView {
+    let resolver: Resolver
+    @ObservedObject var state: CGM.StateModel
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+    @Environment(\.presentationMode) var presentationMode
+
+    var body: some View {
+        NavigationView {
+            Form {
+                Section(
+                    header: Text("Configuration"),
+                    content: {
+                        if state.cgmCurrent.type == .nightscout {
+                            NavigationLink(
+                                destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                                label: { Text("Config Nightscout") }
+                            )
+                        } else if state.cgmCurrent.type == .xdrip {
+                            VStack(alignment: .leading) {
+                                if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
+                                    Text("CGM address :").padding(.top)
+                                    Text(cgmTransmitterDeviceAddress)
+                                } else {
+                                    Text("CGM is not used as heartbeat.").padding(.top)
+                                }
+
+                                HStack(alignment: .center) {
+                                    Text(
+                                        "A heartbeat tells Trio to start a loop cycle. This is required for closed loop."
+                                    )
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                                    .lineLimit(nil)
+                                    Spacer()
+                                }.padding(.vertical)
+                            }
+                        }
+
+                        if let link = state.cgmCurrent.type.externalLink {
+                            Button {
+                                UIApplication.shared.open(link, options: [:], completionHandler: nil)
+                            } label: {
+                                HStack {
+                                    Text("About this source")
+                                    Spacer()
+                                    Image(systemName: "chevron.right")
+                                }
+                            }
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        }
+
+                        if let appURL = state.urlOfApp() {
+                            Button {
+                                UIApplication.shared.open(appURL, options: [:]) { success in
+                                    if !success {
+                                        self.router.alertMessage
+                                            .send(MessageContent(content: "Unable to open the app", type: .warning))
+                                    }
+                                }
+                            }
+                            label: {
+                                HStack {
+                                    Text(state.displayNameOfApp() ?? "-")
+                                    Spacer()
+                                    Image(systemName: "chevron.right")
+                                }
+                            }
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        }
+                    }
+                ).listRowBackground(Color.chart)
+
+                Button {
+                    state.deleteCGM()
+                    presentationMode.wrappedValue.dismiss()
+                } label: {
+                    Text("Delete CGM").foregroundColor(.red)
+                        .frame(maxWidth: .infinity)
+                        .multilineTextAlignment(.center)
+                }
+
+            }.listSectionSpacing(sectionSpacing)
+                .navigationTitle(state.cgmCurrent.displayName)
+                .navigationBarTitleDisplayMode(.inline)
+                .navigationBarItems(leading: Button("Close") {
+                    presentationMode.wrappedValue.dismiss()
+                })
+                .scrollContentBackground(.hidden)
+                .background(appState.trioBackgroundColor(for: colorScheme))
+        }
+    }
+}

+ 26 - 15
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Observation
 import SwiftDate
 import SwiftUI
@@ -16,7 +17,8 @@ extension Calibrations {
 
         var units: GlucoseUnits = .mgdL
 
-        private let context = CoreDataStack.shared.newTaskContext()
+        let backgroundContext = CoreDataStack.shared.newTaskContext()
+        private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
             units = settingsManager.settings.units
@@ -33,21 +35,27 @@ extension Calibrations {
             }
         }
 
-        private func fetchAndProcessGlucose() -> GlucoseStored? {
-            do {
-                debugPrint("Calibrations State Model: \(#function) \(DebuggingIdentifiers.succeeded) fetched glucose")
-                return try context.fetch(GlucoseStored.fetch(
-                    NSPredicate.predicateFor20MinAgo,
-                    ascending: false,
-                    fetchLimit: 1
-                )).first
-            } catch {
-                debugPrint("Calibrations State Model: \(#function) \(DebuggingIdentifiers.failed) failed to fetch glucose")
-                return nil
+        /// - Returns: An array of NSManagedObjectIDs for glucose readings.
+        private func fetchGlucose() async -> [NSManagedObjectID] {
+            let results = await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: GlucoseStored.self,
+                onContext: backgroundContext,
+                predicate: NSPredicate.predicateFor20MinAgo,
+                key: "date",
+                ascending: false,
+                fetchLimit: 1 /// We only need the last value
+            )
+
+            return await backgroundContext.perform {
+                guard let glucoseResults = results as? [GlucoseStored] else {
+                    return []
+                }
+
+                return glucoseResults.map(\.objectID)
             }
         }
 
-        func addCalibration() {
+        @MainActor func addCalibration() async {
             defer {
                 UIApplication.shared.endEditing()
                 setupCalibrations()
@@ -58,9 +66,12 @@ extension Calibrations {
                 glucose = newCalibration.asMgdL
             }
 
-            if let lastGlucose = fetchAndProcessGlucose() {
-                let unfiltered = lastGlucose.glucose
+            let glucoseValuesIds = await fetchGlucose()
+            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
 
+            if let lastGlucose = glucoseObjects.first {
+                let unfiltered = lastGlucose.glucose
                 let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
 
                 calibrationService.addCalibration(calibration)

+ 3 - 1
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -34,7 +34,9 @@ extension Calibrations {
                             Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         Button {
-                            state.addCalibration()
+                            Task {
+                                await state.addCalibration()
+                            }
                         }
                         label: { Text("Add") }
                             .disabled(state.newCalibration <= 0)