Parcourir la source

Refactor TidepoolHealth handling from publisher to observer

Deniz Cengiz il y a 1 jour
Parent
commit
64c9598728

+ 9 - 0
Trio/Sources/Modules/Settings/SettingsStateModel.swift

@@ -20,6 +20,7 @@ extension Settings {
         @Published var debugOptions = false
         @Published var debugOptions = false
         @Published var serviceUIType: ServiceUI.Type?
         @Published var serviceUIType: ServiceUI.Type?
         @Published var setupTidepool = false
         @Published var setupTidepool = false
+        @Published var tidepoolHealth: TidepoolHealth = .unknown
 
 
         private(set) var buildNumber = ""
         private(set) var buildNumber = ""
         private(set) var versionNumber = ""
         private(set) var versionNumber = ""
@@ -42,6 +43,8 @@ extension Settings {
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
 
 
             serviceUIType = TidepoolService.self as? ServiceUI.Type
             serviceUIType = TidepoolService.self as? ServiceUI.Type
+
+            broadcaster.register(TidepoolHealthObserver.self, observer: self)
         }
         }
 
 
         func logItems() -> [URL] {
         func logItems() -> [URL] {
@@ -93,6 +96,12 @@ extension Settings.StateModel: SettingsObserver {
     }
     }
 }
 }
 
 
+extension Settings.StateModel: TidepoolHealthObserver {
+    func tidepoolHealthDidChange(_ health: TidepoolHealth) {
+        tidepoolHealth = health
+    }
+}
+
 extension Settings.StateModel: ServiceOnboardingDelegate {
 extension Settings.StateModel: ServiceOnboardingDelegate {
     func serviceOnboarding(didCreateService service: Service) {
     func serviceOnboarding(didCreateService service: Service) {
         debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")
         debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")

+ 3 - 15
Trio/Sources/Modules/Settings/View/TidepoolStartView.swift

@@ -11,11 +11,6 @@ struct TidepoolStartView: BaseView {
     @State private var decimalPlaceholder: Decimal = 0.0
     @State private var decimalPlaceholder: Decimal = 0.0
     @State private var booleanPlaceholder: Bool = false
     @State private var booleanPlaceholder: Bool = false
 
 
-    /// Mirror of `TidepoolManager.healthPublisher`. Drives the connection
-    /// indicator below — `getTidepoolServiceUI() != nil` only proves the
-    /// service was once configured, not that it's still authenticated.
-    @State private var tidepoolHealth: TidepoolHealth = .unknown
-
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.colorScheme) var colorScheme
     @Environment(AppState.self) var appState
     @Environment(AppState.self) var appState
 
 
@@ -113,16 +108,10 @@ struct TidepoolStartView: BaseView {
         .navigationTitle("Tidepool")
         .navigationTitle("Tidepool")
         .navigationBarTitleDisplayMode(.automatic)
         .navigationBarTitleDisplayMode(.automatic)
         .onAppear(perform: configureView)
         .onAppear(perform: configureView)
-        .onReceive(state.provider.tidepoolManager.healthPublisher) { newHealth in
-            tidepoolHealth = newHealth
-        }
     }
     }
 
 
-    /// Label shown next to the network icon when a Tidepool service is configured.
-    /// Auth failures get an explicit re-login prompt; transient hiccups are
-    /// surfaced but kept gentler so a flaky network doesn't alarm the user.
     private var connectionLabel: String {
     private var connectionLabel: String {
-        switch tidepoolHealth {
+        switch state.tidepoolHealth {
         case .healthy,
         case .healthy,
              .unknown:
              .unknown:
             return String(localized: "Connected to Tidepool")
             return String(localized: "Connected to Tidepool")
@@ -133,9 +122,8 @@ struct TidepoolStartView: BaseView {
         }
         }
     }
     }
 
 
-    /// SF Symbol for the small status dot overlaid on the network icon.
     private var connectionIconName: String {
     private var connectionIconName: String {
-        switch tidepoolHealth {
+        switch state.tidepoolHealth {
         case .healthy,
         case .healthy,
              .unknown:
              .unknown:
             return "checkmark.circle.fill"
             return "checkmark.circle.fill"
@@ -147,7 +135,7 @@ struct TidepoolStartView: BaseView {
     }
     }
 
 
     private var connectionIconColor: Color {
     private var connectionIconColor: Color {
-        switch tidepoolHealth {
+        switch state.tidepoolHealth {
         case .healthy,
         case .healthy,
              .unknown:
              .unknown:
             return .green
             return .green

+ 20 - 17
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -29,6 +29,10 @@ enum TidepoolHealth: Equatable {
     case transient(at: Date)
     case transient(at: Date)
 }
 }
 
 
+protocol TidepoolHealthObserver {
+    func tidepoolHealthDidChange(_ health: TidepoolHealth)
+}
+
 protocol TidepoolManager {
 protocol TidepoolManager {
     func addTidepoolService(service: Service)
     func addTidepoolService(service: Service)
     func getTidepoolServiceUI() -> ServiceUI?
     func getTidepoolServiceUI() -> ServiceUI?
@@ -40,9 +44,6 @@ protocol TidepoolManager {
     func uploadGlucose() async
     func uploadGlucose() async
     func uploadSettings() async
     func uploadSettings() async
     func forceTidepoolDataUpload()
     func forceTidepoolDataUpload()
-    /// Live updates whenever an upload returns; backed by a `CurrentValueSubject`
-    /// so subscribers receive the current value on subscribe.
-    var healthPublisher: AnyPublisher<TidepoolHealth, Never> { get }
 }
 }
 
 
 final class BaseTidepoolManager: TidepoolManager, Injectable {
 final class BaseTidepoolManager: TidepoolManager, Injectable {
@@ -91,15 +92,6 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
 
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?
 
 
-    /// Backing storage for `healthPublisher`. Seeded with `.unknown` so the UI
-    /// shows the optimistic "connected" indicator until the first upload
-    /// returns. Mutated only via `noteUploadSuccess` / `noteUploadFailure`.
-    private let healthSubject = CurrentValueSubject<TidepoolHealth, Never>(.unknown)
-
-    var healthPublisher: AnyPublisher<TidepoolHealth, Never> {
-        healthSubject.eraseToAnyPublisher()
-    }
-
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         self.resolver = resolver
         self.resolver = resolver
         injectServices(resolver)
         injectServices(resolver)
@@ -198,11 +190,11 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
 
     // MARK: - Upload health tracking
     // MARK: - Upload health tracking
 
 
-    /// Records a successful Tidepool upload. Resets `health` to `.healthy(now)`
-    /// — also clears any prior `.authFailed` / `.transient` state, so the UI
-    /// returns to the optimistic indicator on the next success.
+    /// Records a successful Tidepool upload — broadcasts `.healthy(now)` to any
+    /// `TidepoolHealthObserver`, clearing any prior `.authFailed` / `.transient`
+    /// state on the indicator.
     fileprivate func noteUploadSuccess() {
     fileprivate func noteUploadSuccess() {
-        healthSubject.send(.healthy(at: Date()))
+        notifyHealth(.healthy(at: Date()))
     }
     }
 
 
     /// Records a failed Tidepool upload. Routes through `classify` to decide
     /// Records a failed Tidepool upload. Routes through `classify` to decide
@@ -210,7 +202,18 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
     /// transient network/server hiccup. Called from every `.failure` branch
     /// transient network/server hiccup. Called from every `.failure` branch
     /// in the upload completion handlers.
     /// in the upload completion handlers.
     fileprivate func noteUploadFailure(_ error: Error) {
     fileprivate func noteUploadFailure(_ error: Error) {
-        healthSubject.send(classify(error))
+        notifyHealth(classify(error))
+    }
+
+    /// Fans a health change out to all registered `TidepoolHealthObserver`s
+    /// on the main thread. Upload completion handlers can fire from arbitrary
+    /// background queues, and observers update SwiftUI `@Published` state.
+    private func notifyHealth(_ health: TidepoolHealth) {
+        DispatchQueue.main.async {
+            self.broadcaster.notify(TidepoolHealthObserver.self, on: .main) {
+                $0.tidepoolHealthDidChange(health)
+            }
+        }
     }
     }
 
 
     /// Best-effort classification of a Tidepool upload error.
     /// Best-effort classification of a Tidepool upload error.