// // WatchConnectivityManager.swift // Feels // // Central coordinator for Watch Connectivity. // iOS app is the hub - all mood logging flows through here. // import Foundation import WatchConnectivity import WidgetKit import os.log /// Manages Watch Connectivity between iOS and watchOS /// iOS app acts as the central coordinator for all mood logging final class WatchConnectivityManager: NSObject, ObservableObject { static let shared = WatchConnectivityManager() private static let logger = Logger(subsystem: "com.tt.feels", category: "WatchConnectivity") private var session: WCSession? /// Whether the paired device is currently reachable for immediate messaging var isReachable: Bool { session?.isReachable ?? false } private override init() { super.init() if WCSession.isSupported() { session = WCSession.default session?.delegate = self session?.activate() Self.logger.info("WCSession activated") } else { Self.logger.warning("WCSession not supported on this device") } } // MARK: - iOS → Watch #if os(iOS) /// Notify watch to reload its complications func notifyWatchToReload() { guard let session = session, session.activationState == .activated, session.isWatchAppInstalled else { return } let message = ["action": "reloadWidgets"] session.transferUserInfo(message) Self.logger.info("Sent reload notification to watch") } #endif // MARK: - Watch → iOS #if os(watchOS) /// Send mood to iOS app for centralized logging /// Returns true if message was sent, false if fallback to local storage is needed func sendMoodToPhone(mood: Int, date: Date) -> Bool { guard let session = session, session.activationState == .activated else { Self.logger.warning("WCSession not ready") return false } let message: [String: Any] = [ "action": "logMood", "mood": mood, "date": date.timeIntervalSince1970 ] // Use transferUserInfo for guaranteed delivery session.transferUserInfo(message) Self.logger.info("Sent mood \(mood) to iPhone for logging") return true } #endif } // MARK: - WCSessionDelegate extension WatchConnectivityManager: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { if let error = error { Self.logger.error("WCSession activation failed: \(error.localizedDescription)") } else { Self.logger.info("WCSession activation completed: \(activationState.rawValue)") } } #if os(iOS) func sessionDidBecomeInactive(_ session: WCSession) { Self.logger.info("WCSession became inactive") } func sessionDidDeactivate(_ session: WCSession) { Self.logger.info("WCSession deactivated, reactivating...") session.activate() } // iOS receives mood from watch and logs it centrally func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { handleReceivedMessage(userInfo) } func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { handleReceivedMessage(message) } private func handleReceivedMessage(_ message: [String: Any]) { guard let action = message["action"] as? String else { return } switch action { case "logMood": guard let moodRaw = message["mood"] as? Int, let mood = Mood(rawValue: moodRaw), let timestamp = message["date"] as? TimeInterval else { Self.logger.error("Invalid mood message format") return } let date = Date(timeIntervalSince1970: timestamp) Self.logger.info("Received mood \(moodRaw) from watch, logging centrally") Task { @MainActor in // Use MoodLogger for centralized logging with all side effects MoodLogger.shared.logMood(mood, for: date, entryType: .watch) } case "reloadWidgets": Task { @MainActor in WidgetCenter.shared.reloadAllTimelines() } default: break } } #endif #if os(watchOS) // Watch receives reload notification from iOS func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { if userInfo["action"] as? String == "reloadWidgets" { Self.logger.info("Received reload notification from iPhone") Task { @MainActor in WidgetCenter.shared.reloadAllTimelines() } } } func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { if message["action"] as? String == "reloadWidgets" { Task { @MainActor in WidgetCenter.shared.reloadAllTimelines() } } } #endif }