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

Introduce CGM selection dialog and handling to home view

Deniz Cengiz 1 год назад
Родитель
Сommit
40393496a1

+ 4 - 0
Trio/Sources/Modules/Base/BaseStateModel.swift

@@ -11,6 +11,10 @@ protocol StateModel: ObservableObject {
     func view(for screen: Screen) -> AnyView
 }
 
+protocol CGMStateModel: StateModel {
+    var cgmCurrent: CGMType { get }
+}
+
 class BaseStateModel<Provider>: StateModel, Injectable where Provider: Trio.Provider {
     @Injected() var router: Router!
     @Injected() var settingsManager: SettingsManager!

+ 30 - 23
Trio/Sources/Modules/CGM/CGMStateModel.swift

@@ -23,13 +23,21 @@ let cgmDefaultModel = CGMModel(
     subtitle: CGMType.none.subtitle
 )
 
-struct EmptyCompletionNotifying: CompletionNotifying {
+struct OtherCGMSourceCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+class CGMSetupCompletionNotifying: CompletionNotifying {
+    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
+}
+
+class CGMDeletionCompletionNotifying: CompletionNotifying {
     var completionDelegate: (any LoopKitUI.CompletionDelegate)?
 }
 
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
-        @Injected() var cgmManager: FetchGlucoseManager!
+        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var pluginCGMManager: PluginManager!
         @Injected() private var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
@@ -78,7 +86,7 @@ extension CGM {
                         subtitle: cgmPluginInfo.subtitle
                     )
                 } else {
-                    // no more type of plugin available - restart to defaut
+                    // no more type of plugin available - fallback to default model
                     cgmCurrent = cgmDefaultModel
                 }
             default:
@@ -96,8 +104,6 @@ extension CGM {
                 url = URL(string: "spikeapp://")!
             case "http://127.0.0.1:17580":
                 url = URL(string: "diabox://")!
-            //            case CGMType.libreTransmitter.appURL?.absoluteString:
-            //                showModal(for: .libreConfig)
             default: break
             }
 
@@ -107,24 +113,24 @@ extension CGM {
         }
 
         func displayNameOfApp() -> String? {
-            guard cgmManager != nil else { return nil }
+            guard fetchGlucoseManager != nil else { return nil }
             var nameOfApp = "Open Application"
-            switch cgmManager.cgmGlucoseSourceType {
+            switch fetchGlucoseManager.cgmGlucoseSourceType {
             case .plugin:
-                nameOfApp = "Open " + (cgmManager.cgmManager?.localizedTitle ?? "Application")
+                nameOfApp = "Open " + (fetchGlucoseManager.cgmManager?.localizedTitle ?? "Application")
             default:
-                nameOfApp = "Open " + cgmManager.cgmGlucoseSourceType.displayName
+                nameOfApp = "Open " + fetchGlucoseManager.cgmGlucoseSourceType.displayName
             }
             return nameOfApp
         }
 
         func urlOfApp() -> URL? {
-            guard cgmManager != nil else { return nil }
-            switch cgmManager.cgmGlucoseSourceType {
+            guard fetchGlucoseManager != nil else { return nil }
+            switch fetchGlucoseManager.cgmGlucoseSourceType {
             case .plugin:
-                return cgmManager.cgmManager?.appURL
+                return fetchGlucoseManager.cgmManager?.appURL
             default:
-                return cgmManager.cgmGlucoseSourceType.appURL
+                return fetchGlucoseManager.cgmGlucoseSourceType.appURL
             }
         }
 
@@ -134,8 +140,8 @@ extension CGM {
             case .plugin:
                 shouldDisplayCGMSetupSheet.toggle()
             default:
-                cgmManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(EmptyCompletionNotifying())
+                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
             }
         }
 
@@ -143,8 +149,8 @@ extension CGM {
             shouldDisplayCGMSetupSheet = false
 
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
-                self.cgmManager.deleteGlucoseSource()
-                self.completionNotifyingDidComplete(EmptyCompletionNotifying())
+                self.fetchGlucoseManager.deleteGlucoseSource()
+                self.completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
             })
         }
     }
@@ -152,18 +158,19 @@ extension CGM {
 
 extension CGM.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        shouldDisplayCGMSetupSheet = false
-
         // if CGM was deleted
-        if cgmManager.cgmGlucoseSourceType == .none {
+        if fetchGlucoseManager.cgmGlucoseSourceType == .none {
             cgmCurrent = cgmDefaultModel
             settingsManager.settings.cgm = cgmDefaultModel.type
             settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-            cgmManager.deleteGlucoseSource()
+            fetchGlucoseManager.deleteGlucoseSource()
+            shouldDisplayCGMSetupSheet = false
         } else {
             settingsManager.settings.cgm = cgmCurrent.type
             settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
-            cgmManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+            fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+            shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
+                .type == .xdrip || cgmCurrent.type == .enlite
         }
 
         // update glucose source if required
@@ -178,7 +185,7 @@ extension CGM.StateModel: CompletionDelegate {
 extension CGM.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
         // update the glucose source
-        cgmManager.updateGlucoseSource(
+        fetchGlucoseManager.updateGlucoseSource(
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucosePluginId: cgmCurrent.id,
             newManager: manager

+ 12 - 7
Trio/Sources/Modules/CGM/View/CGMRootView.swift

@@ -19,7 +19,7 @@ extension CGM {
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private let cgmOptions: [CGMOption] = [
+        let cgmOptions: [CGMOption] = [
             CGMOption(name: "Dexcom G5", predicate: { $0.type == .plugin && $0.displayName.contains("G5") }),
             CGMOption(name: "Dexcom G6 / ONE", predicate: { $0.type == .plugin && $0.displayName.contains("G6") }),
             CGMOption(name: "Dexcom G7 / ONE+", predicate: { $0.type == .plugin && $0.displayName.contains("G7") }),
@@ -35,7 +35,7 @@ extension CGM {
             CGMOption(name: "xDrip4iOS", predicate: { $0.type == .xdrip })
         ]
 
-        @ViewBuilder var cgmSelectionButtons: some View {
+        var cgmSelectionButtons: some View {
             ForEach(cgmOptions, id: \.name) { option in
                 if let cgm = state.listOfCGM.first(where: option.predicate) {
                     Button(option.name) {
@@ -150,13 +150,18 @@ extension CGM {
                          .simulator,
                          .xdrip:
 
-                        OtherCGMView(resolver: self.resolver, state: state)
+                        OtherCGMView(
+                            resolver: self.resolver,
+                            state: state,
+                            cgmCurrent: state.cgmCurrent,
+                            deleteCGM: state.deleteCGM
+                        )
 
                     case .plugin:
-                        if let cgmFetchManager = state.cgmManager,
-                           let cgmManager = cgmFetchManager.cgmManager,
-                           state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
-                           state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
+                        if let fetchGlucoseManager = state.fetchGlucoseManager,
+                           let cgmManager = fetchGlucoseManager.cgmManager,
+                           state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
+                           state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
                         {
                             CGMSettingsView(
                                 cgmManager: cgmManager,

+ 92 - 87
Trio/Sources/Modules/CGM/View/OtherCGMView.swift

@@ -2,110 +2,115 @@ 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
+extension CGM {
+    struct OtherCGMView: BaseView {
+        let resolver: Resolver
+        @ObservedObject var state: CGM.StateModel
+        let cgmCurrent: CGMModel
+        let deleteCGM: () -> Void
 
-    var body: some View {
-        NavigationView {
-            Form {
-                if state.cgmCurrent.type != .none {
-                    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)
-                                    }
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+        @Environment(\.presentationMode) var presentationMode
 
-                                    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)
-                                }
-                            } else if state.cgmCurrent.type == .simulator {
-                                Text(
-                                    "Trio's glucose simulator does not offer any configuration. Its use is strictly for demonstration purposes only."
-                                )
-                            }
+        var body: some View {
+            NavigationView {
+                Form {
+                    if cgmCurrent.type != .none {
+                        Section(
+                            header: Text("Configuration"),
+                            content: {
+                                if cgmCurrent.type == .nightscout {
+                                    NavigationLink(
+                                        destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                                        label: { Text("Config Nightscout") }
+                                    )
+                                } else if cgmCurrent.type == .xdrip {
+                                    VStack(alignment: .leading) {
+                                        if let cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress {
+                                            Text("CGM address :").padding(.top)
+                                            Text(cgmTransmitterDeviceAddress)
+                                        } else {
+                                            Text("CGM is not used as heartbeat.").padding(.top)
+                                        }
 
-                            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")
+                                        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)
                                     }
+                                } else if cgmCurrent.type == .simulator {
+                                    Text(
+                                        "Trio's glucose simulator does not offer any configuration. Its use is strictly for demonstration purposes only."
+                                    )
                                 }
-                                .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))
+                                if let link = 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)
                                 }
-                                label: {
-                                    HStack {
-                                        Text(state.displayNameOfApp() ?? "-")
-                                        Spacer()
-                                        Image(systemName: "chevron.right")
+
+                                if let appURL = cgmCurrent.type.appURL {
+                                    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(cgmCurrent.displayName)
+                                            Spacer()
+                                            Image(systemName: "chevron.right")
+                                        }
+                                    }
+                                    .frame(maxWidth: .infinity, alignment: .leading)
                                 }
-                                .frame(maxWidth: .infinity, alignment: .leading)
                             }
-                        }
-                    ).listRowBackground(Color.chart)
-                }
+                        ).listRowBackground(Color.chart)
+                    }
 
-                Button {
-                    state.deleteCGM()
-                } label: {
-                    Text("Delete CGM")
-                        .font(.headline)
-                        .foregroundStyle(Color.white)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .frame(height: 35)
+                    Button {
+                        deleteCGM()
+                    } label: {
+                        Text("Delete CGM")
+                            .font(.headline)
+                            .foregroundStyle(Color.white)
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .frame(height: 35)
+                    }
+                    .listRowBackground(Color(.systemRed))
+                    .clipShape(RoundedRectangle(cornerRadius: 8))
                 }
-                .listRowBackground(Color(.systemRed))
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-            }
-            .navigationTitle(state.cgmCurrent.displayName)
-            .navigationBarTitleDisplayMode(.inline)
-            .toolbar {
-                /// proper positioning should be .leading
-                /// but to keep this in line with LoopKit submodules, set placement to .trailing
-                ToolbarItem(placement: .topBarLeading) {
-                    Button("Close") {
-                        presentationMode.wrappedValue.dismiss()
+                .navigationTitle(cgmCurrent.displayName)
+                .navigationBarTitleDisplayMode(.inline)
+                .toolbar {
+                    /// proper positioning should be .leading
+                    /// but to keep this in line with LoopKit submodules, set placement to .trailing
+                    ToolbarItem(placement: .topBarLeading) {
+                        Button("Close") {
+                            presentationMode.wrappedValue.dismiss()
+                        }
                     }
                 }
+                .scrollContentBackground(.hidden)
+                .background(appState.trioBackgroundColor(for: colorScheme))
             }
-            .scrollContentBackground(.hidden)
-            .background(appState.trioBackgroundColor(for: colorScheme))
         }
     }
 }

+ 139 - 10
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -1,3 +1,4 @@
+import CGMBLEKitUI
 import Combine
 import CoreData
 import Foundation
@@ -10,6 +11,7 @@ extension Home {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var broadcaster: Broadcaster!
         @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var pluginCGMManager: PluginManager!
         @ObservationIgnored @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
@@ -17,6 +19,9 @@ extension Home {
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
         @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+
+        var cgmStateModel: CGM.StateModel?
+
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
@@ -44,7 +49,8 @@ extension Home {
         var isExerciseModeActive: Bool = false
         var settingHalfBasalTarget: Decimal = 160
         var percentage: Int = 100
-        var setupPump = false
+        var shouldDisplayPumpSetupSheet = false
+        var shouldDisplayCGMSetupSheet = false
         var errorMessage: String?
         var errorDate: Date?
         var bolusProgress: Decimal?
@@ -92,6 +98,9 @@ extension Home {
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var pumpStatusHighlightMessage: String?
         var cgmAvailable: Bool = false
+        var listOfCGM: [CGMModel] = []
+        var cgmCurrent = cgmDefaultModel
+
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
         var minForecast: [Int] = []
@@ -126,6 +135,11 @@ extension Home {
 
         typealias PumpEvent = PumpEventStored.EventType
 
+        override init() {
+            super.init()
+            cgmStateModel = CGM.StateModel()
+        }
+
         override func subscribe() {
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
@@ -145,6 +159,7 @@ extension Home {
                 // We need to initialize settings and observers first
                 await self.setupSettings()
                 await self.setupPumpSettings()
+                await self.setupCGMSettings()
                 self.registerObservers()
 
                 // The rest can be initialized concurrently
@@ -330,7 +345,7 @@ extension Home {
                         self.battery = nil
                         self.pumpName = ""
                         self.pumpExpiresAtDate = nil
-                        self.setupPump = false
+                        self.shouldDisplayPumpSetupSheet = false
                     } else {
                         self.setupReservoir()
                         self.displayPumpStatusHighlightMessage()
@@ -364,7 +379,6 @@ extension Home {
             displayYgridLines = settingsManager.settings.yGridLines
             thresholdLines = settingsManager.settings.rulerMarks
             totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
-            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             isExerciseModeActive = settingsManager.preferences.exerciseMode
@@ -374,9 +388,77 @@ extension Home {
             maxValue = settingsManager.preferences.autosensMax
         }
 
+        @MainActor private func setupCGMSettings() async {
+            cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
+
+            listOfCGM = (
+                CGMType.allCases.filter { $0 != CGMType.plugin }.map {
+                    CGMModel(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+                } +
+                    pluginCGMManager.availableCGMManagers.map {
+                        CGMModel(
+                            id: $0.identifier,
+                            type: CGMType.plugin,
+                            displayName: $0.localizedTitle,
+                            subtitle: $0.localizedTitle
+                        )
+                    }
+            ).sorted(by: { lhs, rhs in
+                if lhs.displayName == "None" {
+                    return true
+                } else if rhs.displayName == "None" {
+                    return false
+                } else {
+                    return lhs.displayName < rhs.displayName
+                }
+            })
+
+            switch settingsManager.settings.cgm {
+            case .plugin:
+                if let cgmPluginInfo = listOfCGM.first(where: { $0.id == settingsManager.settings.cgmPluginIdentifier }) {
+                    cgmCurrent = CGMModel(
+                        id: settingsManager.settings.cgmPluginIdentifier,
+                        type: .plugin,
+                        displayName: cgmPluginInfo.displayName,
+                        subtitle: cgmPluginInfo.subtitle
+                    )
+                } else {
+                    // no more type of plugin available - fallback to default
+                    cgmCurrent = cgmDefaultModel
+                }
+            default:
+                cgmCurrent = CGMModel(
+                    id: settingsManager.settings.cgm.id,
+                    type: settingsManager.settings.cgm,
+                    displayName: settingsManager.settings.cgm.displayName,
+                    subtitle: settingsManager.settings.cgm.subtitle
+                )
+            }
+        }
+
         func addPump(_ type: PumpConfig.PumpType) {
             setupPumpType = type
-            setupPump = true
+            shouldDisplayPumpSetupSheet = true
+        }
+
+        func addCGM(cgm: CGMModel) {
+            cgmCurrent = cgm
+            switch cgmCurrent.type {
+            case .plugin:
+                shouldDisplayCGMSetupSheet = true
+            default:
+                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
+                completionNotifyingDidComplete(CGMSetupCompletionNotifying())
+            }
+        }
+
+        func deleteCGM() {
+            shouldDisplayCGMSetupSheet = false
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
+                self.fetchGlucoseManager?.deleteGlucoseSource()
+                self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+            })
         }
 
         /// Display the eventual status message provided by the manager of the pump
@@ -547,10 +629,6 @@ extension Home {
                 }
             }
         }
-
-        func openCGM() {
-            router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
-        }
     }
 }
 
@@ -592,6 +670,9 @@ extension Home.StateModel:
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         displayPumpStatusHighlightMessage()
         setupBatteryArray()
+        Task {
+            await setupCGMSettings()
+        }
     }
 
     func preferencesDidChange(_: Preferences) {
@@ -637,8 +718,41 @@ extension Home.StateModel:
 }
 
 extension Home.StateModel: CompletionDelegate {
-    func completionNotifyingDidComplete(_: CompletionNotifying) {
-        setupPump = false
+    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
+        debug(.service, "Completion fired by: \(type(of: notifying))")
+        shouldDisplayCGMSetupSheet = false
+
+        if notifying is CGMSetupCompletionNotifying || notifying is CGMDeletionCompletionNotifying ||
+            notifying is CGMManagerSettingsNavigationViewController || notifying is any SetupTableViewControllerDelegate
+        {
+            if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                debug(.service, "CGMDeletionCompletionNotifying: CGM Deletion Completed")
+
+                cgmCurrent = cgmDefaultModel
+                settingsManager.settings.cgm = cgmDefaultModel.type
+                settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
+                fetchGlucoseManager.deleteGlucoseSource()
+            } else {
+                debug(.service, "CGMSetupCompletionNotifying: CGM Setup Completed")
+
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+
+                shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
+                    .type == .xdrip || cgmCurrent.type == .enlite
+            }
+
+            // update glucose source if required
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            // pump related handling
+            shouldDisplayPumpSetupSheet = false // hides sheet
+        }
     }
 }
 
@@ -658,3 +772,18 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // TODO:
     }
 }
+
+extension Home.StateModel: CGMManagerOnboardingDelegate {
+    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
+        // update the glucose source
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
+    }
+
+    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
+        // nothing to do
+    }
+}

+ 87 - 5
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -32,6 +32,7 @@ extension Home {
         @State var showTreatments = false
         @State var selectedTab: Int = 0
         @State var showPumpSelection: Bool = false
+        @State var showCGMSelection: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
             TimePicker(active: false, hours: 4),
@@ -80,6 +81,32 @@ extension Home {
             }
         }
 
+        let cgmOptions: [CGMOption] = [
+            CGMOption(name: "Dexcom G5", predicate: { $0.type == .plugin && $0.displayName.contains("G5") }),
+            CGMOption(name: "Dexcom G6 / ONE", predicate: { $0.type == .plugin && $0.displayName.contains("G6") }),
+            CGMOption(name: "Dexcom G7 / ONE+", predicate: { $0.type == .plugin && $0.displayName.contains("G7") }),
+            CGMOption(name: "Dexcom Share", predicate: { $0.type == .plugin && $0.displayName.contains("Dexcom Share") }),
+            CGMOption(name: "FreeStyle Libre", predicate: { $0.type == .plugin && $0.displayName == "FreeStyle Libre" }),
+            CGMOption(
+                name: "FreeStyle Libre Demo",
+                predicate: { $0.type == .plugin && $0.displayName == "FreeStyle Libre Demo" }
+            ),
+            CGMOption(name: "Glucose Simulator", predicate: { $0.type == .simulator }),
+            CGMOption(name: "Medtronic Enlite", predicate: { $0.type == .enlite }),
+            CGMOption(name: "Nightscout", predicate: { $0.type == .nightscout }),
+            CGMOption(name: "xDrip4iOS", predicate: { $0.type == .xdrip })
+        ]
+
+        var cgmSelectionButtons: some View {
+            ForEach(cgmOptions, id: \.name) { option in
+                if let cgm = state.listOfCGM.first(where: option.predicate) {
+                    Button(option.name) {
+                        state.addCGM(cgm: cgm)
+                    }
+                }
+            }
+        }
+
         var glucoseView: some View {
             CurrentGlucoseView(
                 timerDate: state.timerDate,
@@ -93,7 +120,11 @@ extension Home {
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
                 .onTapGesture {
-                    state.openCGM()
+                    if !state.cgmAvailable {
+                        showCGMSelection.toggle()
+                    } else {
+                        state.shouldDisplayCGMSetupSheet.toggle()
+                    }
                 }
                 .onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
@@ -118,7 +149,7 @@ extension Home {
                     showPumpSelection.toggle()
                 } else {
                     // sends user to pump settings
-                    state.setupPump.toggle()
+                    state.shouldDisplayPumpSetupSheet.toggle()
                 }
             }
         }
@@ -914,6 +945,10 @@ extension Home {
             .sheet(isPresented: $state.isLoopStatusPresented) {
                 LoopStatusView(state: state)
             }
+            .sheet(isPresented: $state.isLegendPresented) {
+                ChartLegendView(state: state)
+            }
+            // PUMP RELATED
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
@@ -921,7 +956,7 @@ extension Home {
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
                 Button("Pump Simulator") { state.addPump(.simulator) }
             } message: { Text("Select Pump Model") }
-            .sheet(isPresented: $state.setupPump) {
+            .sheet(isPresented: $state.shouldDisplayPumpSetupSheet) {
                 if let pumpManager = state.provider.apsManager.pumpManager {
                     PumpConfig.PumpSettingsView(
                         pumpManager: pumpManager,
@@ -939,8 +974,55 @@ extension Home {
                     )
                 }
             }
-            .sheet(isPresented: $state.isLegendPresented) {
-                ChartLegendView(state: state)
+            // CGM RELATED
+            .confirmationDialog("CGM Model", isPresented: $showCGMSelection) {
+                cgmSelectionButtons
+            } message: {
+                Text("Select CGM Model")
+            }
+            .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {
+                switch state.cgmCurrent.type {
+                case .enlite,
+                     .nightscout,
+                     .none,
+                     .simulator,
+                     .xdrip:
+
+                    // TODO: clean this up
+                    if let cgmState = state.cgmStateModel {
+                        CGM.OtherCGMView(
+                            resolver: self.resolver,
+                            state: cgmState,
+                            cgmCurrent: state.cgmCurrent,
+                            deleteCGM: state.deleteCGM
+                        )
+                    } else {
+                        Text("Error: No CGM State Model Available")
+                    }
+
+                case .plugin:
+                    if let fetchGlucoseManager = state.fetchGlucoseManager,
+                       let cgmManager = fetchGlucoseManager.cgmManager,
+                       state.cgmCurrent.type == fetchGlucoseManager.cgmGlucoseSourceType,
+                       state.cgmCurrent.id == fetchGlucoseManager.cgmGlucosePluginId
+                    {
+                        CGM.CGMSettingsView(
+                            cgmManager: cgmManager,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            unit: state.settingsManager.settings.units,
+                            completionDelegate: state
+                        )
+                    } else {
+                        CGM.CGMSetupView(
+                            CGMType: state.cgmCurrent,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            unit: state.settingsManager.settings.units,
+                            completionDelegate: state,
+                            setupDelegate: state,
+                            pluginCGMManager: self.state.pluginCGMManager
+                        )
+                    }
+                }
             }
         }