Преглед изворни кода

Cleanup and refactor Garmin Manager; attach refactored WatchState

Deniz Cengiz пре 1 година
родитељ
комит
73dda5bd6a

+ 1 - 0
Model/Helper/CustomNotification.swift

@@ -7,6 +7,7 @@ extension Notification.Name {
     static let willUpdateTempTargetConfiguration = Notification.Name("willUpdateTempTargetConfiguration")
     static let didUpdateTempTargetConfiguration = Notification.Name("didUpdateTempTargetConfiguration")
     static let liveActivityOrderDidChange = Notification.Name("liveActivityOrderDidChange")
+    static let openFromGarminConnect = Notification.Name("Notification.Name.openFromGarminConnect")
 }
 
 func awaitNotification(_ name: Notification.Name) async {

+ 10 - 2
Trio.xcodeproj/project.pbxproj

@@ -324,8 +324,8 @@
 		BDA25F202D26D5FE00035F34 /* CarbsInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25F1F2D26D5FB00035F34 /* CarbsInputView.swift */; };
 		BDA25F222D26D62800035F34 /* BolusInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25F212D26D62200035F34 /* BolusInputView.swift */; };
 		BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */; };
-		BDAE40002D372BAD009C12B1 /* WatchState+Requests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */; };
 		BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */; };
+		BDAE40002D372BAD009C12B1 /* WatchState+Requests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
@@ -459,6 +459,8 @@
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
 		DD246F062D2836AA0027DDE0 /* GlucoseTrendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD246F052D2836AA0027DDE0 /* GlucoseTrendView.swift */; };
 		DD2CC85C2D25DA1000445446 /* GlucoseTargetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2CC85B2D25D9CE00445446 /* GlucoseTargetsView.swift */; };
+		DD3078682D42F5CE00DE0490 /* WatchGlucoseObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */; };
+		DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3078692D42F94000DE0490 /* GarminDevice.swift */; };
 		DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */; };
 		DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */; };
 		DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */; };
@@ -1044,8 +1046,8 @@
 		BDA25F1F2D26D5FB00035F34 /* CarbsInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsInputView.swift; sourceTree = "<group>"; };
 		BDA25F212D26D62200035F34 /* BolusInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusInputView.swift; sourceTree = "<group>"; };
 		BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetSetup.swift; sourceTree = "<group>"; };
-		BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchState+Requests.swift"; sourceTree = "<group>"; };
 		BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryEditorView.swift; sourceTree = "<group>"; };
+		BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchState+Requests.swift"; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
@@ -1188,6 +1190,8 @@
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
 		DD246F052D2836AA0027DDE0 /* GlucoseTrendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTrendView.swift; sourceTree = "<group>"; };
 		DD2CC85B2D25D9CE00445446 /* GlucoseTargetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTargetsView.swift; sourceTree = "<group>"; };
+		DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchGlucoseObject.swift; sourceTree = "<group>"; };
+		DD3078692D42F94000DE0490 /* GarminDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminDevice.swift; sourceTree = "<group>"; };
 		DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Bolus.swift"; sourceTree = "<group>"; };
 		DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Meal.swift"; sourceTree = "<group>"; };
 		DD32CF9B2CC82495003686D6 /* TrioRemoteControl+TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+TempTarget.swift"; sourceTree = "<group>"; };
@@ -2051,6 +2055,8 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
+				DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
 				BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */,
 				BD54A9722D281A9C00F9C1EE /* TempTargetPresetWatch.swift */,
 				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
@@ -3768,6 +3774,7 @@
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				BDFD165A2AE40438007F0DDA /* TreatmentsRootView.swift in Sources */,
 				BD54A9732D281ABC00F9C1EE /* TempTargetPresetWatch.swift in Sources */,
+				DD3078682D42F5CE00DE0490 /* WatchGlucoseObject.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */,
 				DD2CC85C2D25DA1000445446 /* GlucoseTargetsView.swift in Sources */,
@@ -3915,6 +3922,7 @@
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
+				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */,
 				DDD163162C4C690300CD525A /* AdjustmentsDataFlow.swift in Sources */,

+ 24 - 0
Trio/Sources/Models/GarminDevice.swift

@@ -0,0 +1,24 @@
+//
+//  GarminDevice.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 23.01.25.
+//
+import ConnectIQ
+
+/// A Codable wrapper around IQDevice so we can persist it easily.
+struct GarminDevice: Codable, Equatable {
+    let id: UUID
+    let modelName: String
+    let friendlyName: String
+    
+    init(iqDevice: IQDevice) {
+        id = iqDevice.uuid
+        modelName = iqDevice.modelName
+        friendlyName = iqDevice.modelName
+    }
+    
+    var iqDevice: IQDevice {
+        IQDevice(id: id, modelName: modelName, friendlyName: friendlyName)
+    }
+}

+ 1 - 1
Trio/Sources/Models/OverridePresetWatch.swift

@@ -7,7 +7,7 @@
 import Foundation
 import SwiftUI
 
-struct OverridePresetWatch: Hashable, Equatable {
+struct OverridePresetWatch: Hashable, Equatable, Codable {
     let name: String
     let isEnabled: Bool
 }

+ 1 - 1
Trio/Sources/Models/TempTargetPresetWatch.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct TempTargetPresetWatch: Hashable, Equatable {
+struct TempTargetPresetWatch: Hashable, Equatable, Codable {
     let name: String
     let isEnabled: Bool
 }

+ 13 - 0
Trio/Sources/Models/WatchGlucoseObject.swift

@@ -0,0 +1,13 @@
+//
+//  WatchGlucoseObject.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 23.01.25.
+//
+import Foundation
+
+struct WatchGlucoseObject: Hashable, Equatable, Codable {
+    let date: Date
+    let glucose: Double
+    let color: String
+}

+ 2 - 2
Trio/Sources/Models/WatchState.swift

@@ -1,13 +1,13 @@
 import Foundation
 import SwiftUI
 
-struct WatchState: Hashable, Equatable, Sendable {
+struct WatchState: Hashable, Equatable, Sendable, Encodable {
     var date: Date
     var currentGlucose: String?
     var currentGlucoseColorString: String?
     var trend: String?
     var delta: String?
-    var glucoseValues: [(date: Date, glucose: Double, color: String)] = []
+    var glucoseValues: [WatchGlucoseObject] = []
     var units: GlucoseUnits = .mgdL
     var iob: String?
     var cob: String?

+ 5 - 3
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -7,7 +7,9 @@ import WatchConnectivity
 
 /// Protocol defining the base functionality for Watch communication
 // TODO: Complete this
-protocol WatchManager {}
+protocol WatchManager {
+    func setupWatchState() async -> WatchState
+}
 
 /// Main implementation of the Watch communication manager
 /// Handles bidirectional communication between iPhone and Apple Watch
@@ -146,7 +148,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     /// Prepares the current state data to be sent to the Watch
     /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
-    private func setupWatchState() async -> WatchState {
+    func setupWatchState() async -> WatchState {
         // Get NSManagedObjectIDs
         let glucoseIds = await fetchGlucose()
         // TODO: - if we want that the watch immediately displays updated cob and iob values when entered via treatment view from phone, we would need to use a predicate here that also filters for NON-ENACTED Determinations
@@ -247,7 +249,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     glucoseColorScheme: self.glucoseColorScheme
                 )
 
-                return (date: glucose.date ?? Date(), glucose: glucoseValue, color: glucoseColor.toHexString())
+                return WatchGlucoseObject(date: glucose.date ?? Date(), glucose: glucoseValue, color: glucoseColor.toHexString())
             }
             .sorted { $0.date < $1.date }
 

+ 276 - 135
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -3,211 +3,352 @@ import ConnectIQ
 import Foundation
 import Swinject
 
+// MARK: - GarminManager Protocol
+
+/// Manages Garmin devices, allowing the app to select devices, update a known device list,
+/// and send watch-state data to connected Garmin watch apps.
 protocol GarminManager {
+    /// Prompts the user to select Garmin devices, returning the chosen devices in a publisher.
+    /// - Returns: A publisher that eventually outputs an array of selected `IQDevice` objects.
     func selectDevices() -> AnyPublisher<[IQDevice], Never>
-    func updateListDevices(devices: [IQDevice])
+    
+    /// Updates the currently tracked device list. This typically persists the device list and
+    /// triggers re-registration for any relevant ConnectIQ events.
+    /// - Parameter devices: The new array of `IQDevice` objects to track.
+    func updateDeviceList(_ devices: [IQDevice])
+    
+    /// Takes raw JSON-encoded watch-state data and dispatches it to any connected watch apps.
+    /// - Parameter data: The JSON-encoded data representing the watch state.
+    func sendWatchStateData(_ data: Data)
+    
+    /// The devices currently known to the app. May be loaded from disk or user selection.
     var devices: [IQDevice] { get }
-    func sendState(_ data: Data)
-    var stateRequet: (() -> (Data))? { get set }
+    
+    /// An async closure that, when called, returns the latest watch state data (encoded as `Data`)
+    /// to be sent to the watch on demand (e.g., when the watch pings "status").
+    var watchStateDataProvider: (() async -> Data)? { get set }
 }
 
-extension Notification.Name {
-    static let openFromGarminConnect = Notification.Name("Notification.Name.openFromGarminConnect")
-}
+// MARK: - BaseGarminManager
 
+/// Concrete implementation of `GarminManager` that handles device registration, data persistence,
+/// and sending watch-state updates via the Garmin ConnectIQ SDK.
 final class BaseGarminManager: NSObject, GarminManager, Injectable {
+    
+    // MARK: - Config
+    
     private enum Config {
+        /// Example watchface UUID
         static let watchfaceUUID = UUID(uuidString: "EC3420F6-027D-49B3-B45F-D81D6D3ED90A")
+        /// Example data field UUID
         static let watchdataUUID = UUID(uuidString: "71CF0982-CA41-42A5-8441-EA81D36056C3")
     }
-
-    private let connectIQ = ConnectIQ.sharedInstance()
-
-    private let router = TrioApp.resolver.resolve(Router.self)!
-
+    
+    // MARK: - Dependencies & Properties
+    
+    /// NotificationCenter used for responding to `.openFromGarminConnect` notifications.
     @Injected() private var notificationCenter: NotificationCenter!
-
-    @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [CodableDevice] = []
-
-    private var watchfaces: [IQApp] = []
-
-    var stateRequet: (() -> (Data))?
-
-    private let stateSubject = PassthroughSubject<NSDictionary, Never>()
-
+    @Injected() private var watchManager: WatchManager!
+    
+    /// Persists the user’s device list between app launches.
+    @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [GarminDevice] = []
+    
+    /// Router for presenting alerts or navigation flows (injected via Swinject).
+    private let router: Router
+    
+    /// Garmin ConnectIQ shared instance for all watch interactions.
+    private let connectIQ = ConnectIQ.sharedInstance()
+    
+    /// Keeps references to watch apps (both watchface & data field) for each registered device.
+    private var watchApps: [IQApp] = []
+    
+    /// A subject that dispatches watch-state dictionaries to the watch on a throttled schedule.
+    private let watchStateSubject = PassthroughSubject<NSDictionary, Never>()
+    
+    /// A set of Combine cancellables for managing the lifecycle of various subscriptions.
+    private var cancellables = Set<AnyCancellable>()
+    
+    /// Holds a promise used when the user is selecting devices (via `showDeviceSelection()`).
+    private var deviceSelectionPromise: Future<[IQDevice], Never>.Promise?
+    
+    /// Async closure returning JSON-encoded watch state. Called when the watch pings "status".
+    var watchStateDataProvider: (() async -> Data)?
+    
+    /// Array of Garmin `IQDevice` objects currently being tracked.
+    /// Changing this property triggers re-registration and updates persisted devices.
     private(set) var devices: [IQDevice] = [] {
         didSet {
-            persistedDevices = devices.map(CodableDevice.init)
-            watchfaces = []
-            devices.forEach { device in
-                connectIQ?.register(forDeviceEvents: device, delegate: self)
-                let watchfaceApp = IQApp(
-                    uuid: Config.watchfaceUUID,
-                    store: UUID(),
-                    device: device
-                )
-                let watchDataFieldApp = IQApp(
-                    uuid: Config.watchdataUUID,
-                    store: UUID(),
-                    device: device
-                )
-                watchfaces.append(watchfaceApp!)
-                watchfaces.append(watchDataFieldApp!)
-                connectIQ?.register(forAppMessages: watchfaceApp, delegate: self)
-            }
+            // Persist newly updated device list
+            persistedDevices = devices.map(GarminDevice.init)
+            // Re-register for events, app messages, etc.
+            registerDevices(devices)
         }
     }
-
-    private var lifetime = Lifetime()
-    private var selectPromise: Future<[IQDevice], Never>.Promise?
-
+    
+    // MARK: - Initialization
+    
+    /// Creates a new `BaseGarminManager`, injecting required services and restoring any persisted devices.
+    /// - Parameter resolver: Swinject resolver for injecting dependencies like the Router.
     init(resolver: Resolver) {
+        self.router = resolver.resolve(Router.self)!
         super.init()
+        
+        // Initialize ConnectIQ with a custom URL scheme and override delegate
         connectIQ?.initialize(withUrlScheme: "Trio", uiOverrideDelegate: self)
+        
+        // Inject any property wrappers that need the resolver
         injectServices(resolver)
+        
+        // Restore previously persisted devices
         restoreDevices()
+        
+        // Subscribe to relevant notifications and watch-state changes
         subscribeToOpenFromGarminConnect()
-        setupApplications()
-        subscribeState()
+        subscribeToWatchState()
     }
-
+    
+    // MARK: - Device & App Registration
+    
+    /// Registers the given devices for ConnectIQ events (device status changes) and watch app messages.
+    /// It also creates and registers watch apps (watchface + data field) for each device.
+    /// - Parameter devices: The devices to register.
+    private func registerDevices(_ devices: [IQDevice]) {
+        // Clear out old references
+        watchApps.removeAll()
+        
+        for device in devices {
+            // Listen for device-level status changes
+            connectIQ?.register(forDeviceEvents: device, delegate: self)
+            
+            // Create a watchface app
+            guard
+                let watchfaceUUID = Config.watchfaceUUID,
+                let watchfaceApp = IQApp(uuid: watchfaceUUID, store: UUID(), device: device)
+            else {
+                debug(.watchManager, "Garmin: Could not create watchface app for device \(String(describing: device.uuid))")
+                continue
+            }
+            
+            // Create a watch data field app
+            guard
+                let watchdataUUID = Config.watchdataUUID,
+                let watchDataFieldApp = IQApp(uuid: watchdataUUID, store: UUID(), device: device)
+            else {
+                debug(.watchManager, "Garmin: Could not create data-field app for device \(String(describing: device.uuid))")
+                continue
+            }
+            
+            // Track both apps for potential messages
+            watchApps.append(watchfaceApp)
+            watchApps.append(watchDataFieldApp)
+            
+            // Register to receive app-messages from the watchface (if you also want data-field messages,
+            // register that, too)
+            connectIQ?.register(forAppMessages: watchfaceApp, delegate: self)
+        }
+    }
+    
+    /// Restores previously persisted devices from local storage into `devices`.
+    private func restoreDevices() {
+        devices = persistedDevices.map(\.iqDevice)
+    }
+    
+    // MARK: - Combine Subscriptions
+    
+    /// Subscribes to the `.openFromGarminConnect` notification, parsing devices from the given URL
+    /// and updating the device list accordingly.
     private func subscribeToOpenFromGarminConnect() {
         notificationCenter
             .publisher(for: .openFromGarminConnect)
-            .sink { notification in
-                guard let url = notification.object as? URL else { return }
-                self.parseDevicesFor(url: url)
+            .sink { [weak self] notification in
+                guard
+                    let self = self,
+                    let url = notification.object as? URL
+                else { return }
+                
+                self.parseDevices(for: url)
             }
-            .store(in: &lifetime)
+            .store(in: &cancellables)
     }
-
-    private func subscribeState() {
-        func sendToWatchface(state: NSDictionary) {
-            watchfaces.forEach { app in
-                connectIQ?.getAppStatus(app) { status in
-                    guard status?.isInstalled ?? false else {
-                        debug(.service, "Garmin: watchface app not installed")
-                        return
-                    }
-                    debug(.service, "Garmin: sending message to watchface")
-                    self.sendMessage(state, to: app)
-                }
-            }
-        }
-
-        stateSubject
+    
+    /// Subscribes to any watch-state dictionaries published via `watchStateSubject`, and throttles them
+    /// so updates aren’t sent too frequently. Each update triggers a broadcast to all watch apps.
+    private func subscribeToWatchState() {
+        watchStateSubject
             .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
-            .sink { state in
-                sendToWatchface(state: state)
+            .sink { [weak self] state in
+                self?.broadcastStateToWatchApps(state)
             }
-            .store(in: &lifetime)
+            .store(in: &cancellables)
     }
-
-    private func restoreDevices() {
-        devices = persistedDevices.map(\.iqDevice)
+    
+    // MARK: - Parsing & Broadcasting
+    
+    /// Parses devices from a Garmin Connect URL and updates our `devices` property.
+    /// - Parameter url: The URL provided by Garmin Connect containing device selection info.
+    private func parseDevices(for url: URL) {
+        let parsed = connectIQ?.parseDeviceSelectionResponse(from: url) as? [IQDevice]
+        devices = parsed ?? []
+        
+        // Fulfill any pending promise in case this is in response to `selectDevices()`.
+        deviceSelectionPromise?(.success(devices))
+        deviceSelectionPromise = nil
     }
-
-    private func parseDevicesFor(url: URL) {
-        devices = connectIQ?.parseDeviceSelectionResponse(from: url) as? [IQDevice] ?? []
-        selectPromise?(.success(devices))
-        selectPromise = nil
-    }
-
-    private func setupApplications() {
-        devices.forEach { _ in
+    
+    /// Sends the given state dictionary to all known watch apps (watchface & data field) by checking
+    /// if each app is installed and then sending messages asynchronously.
+    /// - Parameter state: The dictionary representing the watch state to be broadcast.
+    private func broadcastStateToWatchApps(_ state: NSDictionary) {
+        watchApps.forEach { app in
+            connectIQ?.getAppStatus(app) { [weak self] status in
+                guard status?.isInstalled == true else {
+                    debug(.watchManager, "Garmin: App not installed on device: \(String(describing: app.uuid))")
+                    return
+                }
+                debug(.watchManager, "Garmin: Sending watch-state to app \(String(describing: app.uuid))")
+                self?.sendMessage(state, to: app)
+            }
         }
     }
-
+    
+    // MARK: - GarminManager Conformance
+    
+    /// Prompts the user to select one or more Garmin devices, returning a publisher that emits
+    /// the final array of selected devices once the user finishes selection.
+    /// - Returns: An `AnyPublisher` emitting `[IQDevice]` on success, or empty array on error/timeout.
     func selectDevices() -> AnyPublisher<[IQDevice], Never> {
-        Future { promise in
-            self.selectPromise = promise
+        Future { [weak self] promise in
+            guard let self = self else {
+                // If self is gone, just resolve with an empty array
+                promise(.success([]))
+                return
+            }
+            // Store the promise so we can fulfill it when the user selects devices
+            self.deviceSelectionPromise = promise
+            
+            // Show Garmin's default device selection UI
             self.connectIQ?.showDeviceSelection()
         }
-        .timeout(120, scheduler: DispatchQueue.main)
+        .timeout(.seconds(120), scheduler: DispatchQueue.main)
         .replaceEmpty(with: [])
         .eraseToAnyPublisher()
     }
-
-    func updateListDevices(devices: [IQDevice]) {
+    
+    /// Updates the manager’s list of devices, typically after user selection or manual changes.
+    /// - Parameter devices: The new array of `IQDevice` objects to track.
+    func updateDeviceList(_ devices: [IQDevice]) {
         self.devices = devices
     }
-
-    func sendState(_ data: Data) {
-        guard let object = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary else {
+    
+    /// Converts the given JSON data into an NSDictionary and sends it to all known watch apps.
+    /// - Parameter data: JSON-encoded data representing the latest watch state. If decoding fails,
+    ///   the method logs an error and does nothing else.
+    func sendWatchStateData(_ data: Data) {
+        guard
+            let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
+            let dict = jsonObject as? NSDictionary
+        else {
+            debug(.watchManager, "Garmin: Invalid JSON for watch-state data")
             return
         }
-        stateSubject.send(object)
+        watchStateSubject.send(dict)
     }
-
+    
+    // MARK: - Helper: Sending Messages
+    
+    /// Sends a message to a given IQApp with optional progress and completion callbacks.
+    /// - Parameters:
+    ///   - msg: The dictionary to send to the watch app.
+    ///   - app: The `IQApp` instance representing the watchface or data field.
     private func sendMessage(_ msg: NSDictionary, to app: IQApp) {
-        connectIQ?.sendMessage(msg, to: app, progress: { _, _ in
-            // debug(.service, "Garmin: sending progress: \(Int(Double(sent) / Double(all) * 100)) %")
-        }, completion: { result in
-            if result == .success {
-                debug(.service, "Garmin: message sent")
-            } else {
-                debug(.service, "Garmin: message failed")
+        connectIQ?.sendMessage(
+            msg,
+            to: app,
+            progress: { _, _ in
+                // Optionally track progress here
+            },
+            completion: { result in
+                switch result {
+                case .success:
+                    debug(.watchManager, "Garmin: Successfully sent message to \(String(describing: app.uuid))")
+                default:
+                    debug(.watchManager, "Garmin: Unknown result or failed to send message to \(String(describing: app.uuid))")
+                }
             }
-        })
+        )
     }
 }
 
-extension BaseGarminManager: IQUIOverrideDelegate {
+// MARK: - Extensions
+
+extension BaseGarminManager: IQUIOverrideDelegate, IQDeviceEventDelegate, IQAppMessageDelegate {
+    
+    // MARK: - IQUIOverrideDelegate
+    
+    /// Called if the Garmin Connect Mobile app is not installed or otherwise not available.
+    /// Typically, you would show an alert or prompt the user to install the app from the store.
     func needsToInstallConnectMobile() {
-        debug(.apsManager, NSLocalizedString("Garmin is not available", comment: ""))
+        debug(.apsManager, "Garmin is not available")
         let messageCont = MessageContent(
-            content: NSLocalizedString(
-                "The app Garmin Connect must be installed to use for Trio.\n Go to App Store to download it",
-                comment: ""
-            ),
+            content: "The app Garmin Connect must be installed to use Trio.\nGo to the App Store to download it.",
             type: .warning,
             subtype: .misc,
-            title: NSLocalizedString("Garmin is not available", comment: "")
+            title: "Garmin is not available"
         )
         router.alertMessage.send(messageCont)
     }
-}
-
-extension BaseGarminManager: IQDeviceEventDelegate {
+    
+    // MARK: - IQDeviceEventDelegate
+    
+    /// Called whenever the status of a registered Garmin device changes (e.g., connected, not found, etc.).
+    /// - Parameters:
+    ///   - device: The device whose status has changed.
+    ///   - status: The new status for the device.
     func deviceStatusChanged(_ device: IQDevice, status: IQDeviceStatus) {
         switch status {
         case .invalidDevice:
-            debug(.service, "Garmin: invalidDevice, Device: \(device.uuid!)")
+            debug(.watchManager, "Garmin: invalidDevice (\(String(describing: device.uuid)))")
         case .bluetoothNotReady:
-            debug(.service, "Garmin: bluetoothNotReady, Device: \(device.uuid!)")
+            debug(.watchManager, "Garmin: bluetoothNotReady (\(String(describing: device.uuid)))")
         case .notFound:
-            debug(.service, "Garmin: notFound, Device: \(device.uuid!)")
+            debug(.watchManager, "Garmin: notFound (\(String(describing: device.uuid)))")
         case .notConnected:
-            debug(.service, "Garmin: notConnected, Device: \(device.uuid!)")
+            debug(.watchManager, "Garmin: notConnected (\(String(describing: device.uuid)))")
         case .connected:
-            debug(.service, "Garmin: connected, Device: \(device.uuid!)")
+            debug(.watchManager, "Garmin: connected (\(String(describing: device.uuid)))")
         @unknown default:
-            debug(.service, "Garmin: unknown state, Device: \(device.uuid!)")
+            debug(.watchManager, "Garmin: unknown state (\(String(describing: device.uuid)))")
         }
     }
-}
+    
+    // MARK: - IQAppMessageDelegate
 
-extension BaseGarminManager: IQAppMessageDelegate {
+    /// Called when a message arrives from a Garmin watch app (watchface or data field).
+    /// If the watch requests a "status" update, we call `setupWatchState()` asynchronously
+    /// and re-send the watch state data.
     func receivedMessage(_ message: Any, from app: IQApp) {
-        print("ASDF: got message: \(message) from app: \(app.uuid!)")
-        if let status = message as? String, status == "status", let watchState = stateRequet?() {
-            sendState(watchState)
-        }
-    }
-}
-
-struct CodableDevice: Codable, Equatable {
-    let id: UUID
-    let modelName: String
-    let friendlyName: String
+        debug(.watchManager, "Garmin: Received message \(message) from app \(String(describing: app.uuid))")
 
-    init(iqDevice: IQDevice) {
-        id = iqDevice.uuid
-        modelName = iqDevice.modelName
-        friendlyName = iqDevice.modelName
+        Task {
+            // Check if the message is literally the string "status"
+            guard
+                let statusString = message as? String,
+                statusString == "status"
+            else {
+                return
+            }
+            
+            do {
+                // Fetch the latest watch state (async) and encode it to JSON data
+                let watchState = await watchManager.setupWatchState()
+                let watchStateData = try JSONEncoder().encode(watchState)
+                
+                // Now send that JSON to the watch
+                sendWatchStateData(watchStateData)
+            } catch {
+                warning(.service, "Garmin: Cannot encode watch state: \(error)")
+            }
+        }
     }
 
-    var iqDevice: IQDevice {
-        IQDevice(id: id, modelName: modelName, friendlyName: friendlyName)
-    }
 }