瀏覽代碼

Merge pull request #397 from kingst/fix-settings-cgm-delete

Fix CGM add / delete logic for the settings flow
Deniz Cengiz 1 年之前
父節點
當前提交
dfc57729b3

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -204,6 +204,7 @@
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
+		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
@@ -925,6 +926,7 @@
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
@@ -1755,6 +1757,7 @@
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
+				3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */,
 				58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */,
 				3811DE2C25C9D49500A708ED /* View */,
 			);
@@ -3880,6 +3883,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
+				3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,

+ 2 - 2
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -122,12 +122,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @MainActor func deleteGlucoseSource() async {
         cgmManager = nil
         glucoseSource = nil
+        settingsManager.settings.cgm = cgmDefaultModel.type
+        settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
         updateGlucoseSource(
             cgmGlucoseSourceType: cgmDefaultModel.type,
             cgmGlucosePluginId: cgmDefaultModel.id
         )
-        settingsManager.settings.cgm = cgmDefaultModel.type
-        settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
     }
 
     func saveConfigManager() {

+ 60 - 47
Trio/Sources/Modules/CGMSettings/CGMSettingsStateModel.swift

@@ -4,6 +4,9 @@ import G7SensorKit
 import LoopKitUI
 import SwiftUI
 
+/// For a full description of the events that can happen for the CGM lifecycle, see comment at the top
+/// of HomeStateModel+CGM since these are the same events
+
 struct CGMModel: Identifiable, Hashable {
     var id: String
     var type: CGMType
@@ -23,18 +26,6 @@ let cgmDefaultModel = CGMModel(
     subtitle: CGMType.none.subtitle
 )
 
-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 CGMSettings {
     final class StateModel: BaseStateModel<Provider> {
         // Singleton implementation
@@ -49,7 +40,7 @@ extension CGMSettings {
 
         @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var pluginCGMManager: PluginManager!
-        @Injected() private var broadcaster: Broadcaster!
+        @Injected() var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
 
         @Published var units: GlucoseUnits = .mgdL
@@ -60,8 +51,11 @@ extension CGMSettings {
         @Published var listOfCGM: [CGMModel] = []
         @Published var url: URL?
 
+        var shouldRunDeleteOnSettingsChange = true
+
         override func subscribe() {
             units = settingsManager.settings.units
+            broadcaster.register(SettingsObserver.self, observer: self)
 
             // collect the list of CGM available with plugins and CGMType defined manually
             listOfCGM = (
@@ -122,28 +116,36 @@ extension CGMSettings {
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
         }
 
+        // this function will get called for all CGM types (plugin and non plugin)
         func addCGM(cgm: CGMModel) {
             cgmCurrent = cgm
-            switch cgmCurrent.type {
+            switch cgm.type {
             case .plugin:
                 shouldDisplayCGMSetupSheet.toggle()
             default:
-                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
+                // non plugin CGM types should be considered onboarded right away
+                shouldDisplayCGMSetupSheet = true
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = ""
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+                broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
             }
         }
 
+        // Note: This function does _not_ get called for plugin CGMs
+        // instead, they will get cgmManagerWantsDeletion events which
+        // are handled by PluginSource
         func deleteCGM() {
-            fetchGlucoseManager.performOnCGMManagerQueue {
-                // Call plugin functionality on the manager queue (or at least attempt to)
-                Task {
-                    await self.fetchGlucoseManager?.deleteGlucoseSource()
-                }
+            Task {
+                await self.fetchGlucoseManager?.deleteGlucoseSource()
 
-                // UI updates go back to Main
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.shouldDisplayCGMSetupSheet = false
-                    self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+                    broadcaster.notify(GlucoseObserver.self, on: .main) {
+                        $0.glucoseDidUpdate([])
+                    }
                 }
             }
         }
@@ -152,40 +154,36 @@ extension CGMSettings {
 
 extension CGMSettings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        // if CGM was deleted
-        if fetchGlucoseManager.cgmGlucoseSourceType == .none {
-            cgmCurrent = cgmDefaultModel
-            settingsManager.settings.cgm = cgmDefaultModel.type
-            settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-            Task {
-                await fetchGlucoseManager.deleteGlucoseSource()
-            }
-            shouldDisplayCGMSetupSheet = false
-        } else {
-            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([])
+        Task {
+            // this sleep is because this event and cgmManagerWantsDeletion
+            // are called in parallel.
+            try await Task.sleep(for: .seconds(0.2))
+            await MainActor.run {
+                if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                    cgmCurrent = cgmDefaultModel
+                }
             }
         }
+        shouldDisplayCGMSetupSheet = false
     }
 }
 
 extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
-        // update the glucose source
+        // cgmCurrent should have been set in addCGM
+        debug(.service, "didCreateCGMManager called \(cgmCurrent)")
+        settingsManager.settings.cgm = cgmCurrent.type
+        settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
         fetchGlucoseManager.updateGlucoseSource(
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucosePluginId: cgmCurrent.id,
             newManager: manager
         )
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
     }
 
     func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
@@ -193,8 +191,23 @@ extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     }
 }
 
-extension CGMSettings.StateModel {
+extension CGMSettings.StateModel: SettingsObserver {
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
+        // Deletes are handled differently for plugins vs non plugins
+        // but both will call deleteGlucoseSource on the fetchGlucoseManager
+        // so we listen for changes to the cgm setting and update our internal
+        // state accordingly
+        if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
+            shouldRunDeleteOnSettingsChange = false
+            cgmCurrent = cgmDefaultModel
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            shouldRunDeleteOnSettingsChange = true
+        }
     }
 }

+ 5 - 1
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -161,7 +161,11 @@ extension CGMSettings {
                                 completionDelegate: state,
                                 setupDelegate: state,
                                 pluginCGMManager: self.state.pluginCGMManager
-                            )
+                            ).onDisappear {
+                                if state.fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                                    state.cgmCurrent = cgmDefaultModel
+                                }
+                            }
                         }
                     }
                 }

+ 4 - 6
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -140,12 +140,10 @@ extension CGMSettings {
                                 .padding(.vertical)
                         }
 
-                        if state.url == nil {
-                            NavigationLink(
-                                destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
-                                label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
-                            )
-                        }
+                        NavigationLink(
+                            destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                            label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
+                        )
                     }
                 ).listRowBackground(Color.chart)
 

+ 77 - 0
Trio/Sources/Modules/Home/HomeStateModel+CGM.swift

@@ -0,0 +1,77 @@
+import LoopKitUI
+
+/// Notes on the CGM lifecycle:
+/// There are two classes of CGM devices: plugins and non-plugins. Plugins are implemented using
+/// LoopKit APIs and include most hardware CGMs like Dexcom G6, G7, Libre, and so on. Non-plugins
+/// drivers are implemented directly in Trio, and include the CGM Simulator and Nightscout CGM. For
+/// these different CGMs, there are a few different events, handled in different places, that happen to
+/// signify a change in the CGM lifecycle.
+///
+/// Both:
+/// - addCGM function invocation: Called by the UI in response to a user clicking the "add CGM" button
+///
+/// Non-plugins only:
+/// - deleteCGM function invocation: Called by the CGM View in response to a user clicking the "delete CGM" button
+///
+/// Plugins only:
+/// - completionNotifyingDidComplete: Called by the CGM driver to signify that Trio should close its UIViewController
+/// - cgmManagerOnboarding didCreateCGMManager: Called by the CGM driver after adding a new CGM
+/// - cgmManagerWantsDeletion: Called by the CGM driver when the user asks to delete a CGM
+/// There are no ordering constraints between completionNotifyingDidComplete and the other two
+/// Plugin events (it's up to the implementation of each individual driver). For example, the G7 driver invokes
+/// cgmManagerWantsDeletion on the delegate's queue while calling completionNotifyingDidComplete in parallel
+/// on the main queue.
+///
+/// In additinon to having different events for different types of CGMs, the handling of these events is spread out
+/// across various state managers, like HomeStateModel, CGMSettingsStateModel, and PluginSource.
+///
+/// There is CGM state in the HomeStateModel and CGMSettingsStateModel, FetchGlucoseManager, and
+/// SettingsManger
+///
+/// The flow for adding a CGM:
+/// - Non-plugin: addCGM (considered onboarded at this point)
+/// - Plugin: addCGM -> cgmManagerOnboarding (after success)
+///
+/// For deleting a CGM:
+/// - Non-plugin: deleteCGM (in HomeStateModel and CGMSettingsStateModel)
+/// - Plugin: cgmManagerWantsDeletion (in PluginSource)
+/// Then, both non-plugin and plugin:  set settings.cgm (in FetchGlucoseManager) ->
+///     settingsDidChange (in HomeStateModel and CGMSettingsStateModel)
+
+extension Home.StateModel: CompletionDelegate {
+    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
+        debug(.service, "Completion fired by: \(type(of: notifying))")
+        Task {
+            // this sleep is because this event and cgmManagerWantsDeletion
+            // are called in parallel.
+            try await Task.sleep(for: .seconds(0.2))
+            await MainActor.run {
+                if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                    cgmCurrent = cgmDefaultModel
+                }
+            }
+        }
+        shouldDisplayCGMSetupSheet = false
+    }
+}
+
+extension Home.StateModel: CGMManagerOnboardingDelegate {
+    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
+        settingsManager.settings.cgm = cgmCurrent.type
+        settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
+    }
+
+    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
+        // nothing to do
+    }
+}

+ 26 - 64
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -103,6 +103,7 @@ extension Home {
         var cgmAvailable: Bool = false
         var listOfCGM: [CGMModel] = []
         var cgmCurrent = cgmDefaultModel
+        var shouldRunDeleteOnSettingsChange = true
 
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
@@ -455,8 +456,13 @@ extension Home {
             case .plugin:
                 shouldDisplayCGMSetupSheet = true
             default:
-                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(CGMSetupCompletionNotifying())
+                shouldDisplayCGMSetupSheet = true
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = ""
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+                broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
             }
         }
 
@@ -465,12 +471,14 @@ extension Home {
                 // Call plugin functionality on the manager queue (or at least attempt to)
                 Task {
                     await self.fetchGlucoseManager?.deleteGlucoseSource()
-                }
 
-                // UI updates go back to Main
-                DispatchQueue.main.async {
-                    self.shouldDisplayCGMSetupSheet = false
-                    self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+                    // UI updates go back to Main
+                    await MainActor.run {
+                        self.shouldDisplayCGMSetupSheet = false
+                        self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                            $0.glucoseDidUpdate([])
+                        }
+                    }
                 }
             }
         }
@@ -643,6 +651,17 @@ extension Home.StateModel:
         Task {
             await setupCGMSettings()
         }
+        if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
+            shouldRunDeleteOnSettingsChange = false
+            cgmCurrent = cgmDefaultModel
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            shouldRunDeleteOnSettingsChange = true
+        }
     }
 
     func preferencesDidChange(_: Preferences) {
@@ -685,48 +704,6 @@ extension Home.StateModel:
     }
 }
 
-extension Home.StateModel: CompletionDelegate {
-    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 ||
-            notifying is any CGMManagerOnboarding
-        {
-            if fetchGlucoseManager.cgmGlucoseSourceType == .none {
-                debug(.service, "CGMDeletionCompletionNotifying: CGM Deletion Completed")
-
-                cgmCurrent = cgmDefaultModel
-                settingsManager.settings.cgm = cgmDefaultModel.type
-                settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-                Task {
-                    await 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
-        }
-    }
-}
-
 extension Home.StateModel: PumpManagerOnboardingDelegate {
     func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
         provider.apsManager.pumpManager = pumpManager
@@ -743,18 +720,3 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // nothing to do
     }
 }
-
-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
-    }
-}