// // WatchConnectivityManager.swift // Reflect // // Central coordinator for Watch Connectivity. // Used for immediate UI updates (Live Activity, widget refresh). // Data persistence is handled by CloudKit sync. // import Foundation import WatchConnectivity import WidgetKit import os.log /// Manages Watch Connectivity between iOS and watchOS /// Used for immediate notifications, not data persistence (CloudKit handles that) final class WatchConnectivityManager: NSObject, ObservableObject { static let shared = WatchConnectivityManager() private static let logger = Logger(subsystem: "com.88oakapps.reflect", category: "WatchConnectivity") private var session: WCSession? #if os(watchOS) /// Pending moods to send when session activates private var pendingMoods: [(mood: Int, date: Date)] = [] #endif /// 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 activation requested") } 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 immediate side effects (Live Activity, etc.) /// Data persistence is handled by CloudKit - this is just for immediate UI updates func sendMoodToPhone(mood: Int, date: Date) -> Bool { guard let session = session else { Self.logger.warning("WCSession not ready - session is nil") return false } guard session.activationState == .activated else { pendingMoods.append((mood: mood, date: date)) if session.activationState == .notActivated { session.activate() } Self.logger.warning("WCSession not activated, queued mood for later") return false } let message: [String: Any] = [ "action": "logMood", "mood": mood, "date": date.timeIntervalSince1970 ] // Try immediate message first if iPhone is reachable if session.isReachable { session.sendMessage(message, replyHandler: nil) { error in Self.logger.warning("sendMessage failed: \(error.localizedDescription), using transferUserInfo") session.transferUserInfo(message) } Self.logger.info("Sent mood \(mood) to iPhone via sendMessage") } else { session.transferUserInfo(message) Self.logger.info("Sent mood \(mood) to iPhone via transferUserInfo") } 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(watchOS) if activationState == .activated && !pendingMoods.isEmpty { Self.logger.info("Sending \(self.pendingMoods.count) pending mood(s)") for pending in pendingMoods { _ = sendMoodToPhone(mood: pending.mood, date: pending.date) } pendingMoods.removeAll() } #endif } } #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() } func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { Self.logger.info("Received userInfo from watch") handleReceivedMessage(userInfo) } func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { Self.logger.info("Received message from watch") handleReceivedMessage(message) } func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { Self.logger.info("Received message from watch (with reply)") handleReceivedMessage(message) replyHandler(["status": "received"]) } private func handleReceivedMessage(_ message: [String: Any]) { Task { @MainActor in guard let action = message["action"] as? String else { Self.logger.error("No action in message") 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("Processing mood \(moodRaw) from watch for \(date)") MoodLogger.shared.logMood(mood, for: date, entryType: .watch) case "reloadWidgets": Self.logger.info("Received reloadWidgets from watch") WidgetCenter.shared.reloadAllTimelines() default: Self.logger.warning("Unknown action: \(action)") } } } #endif #if os(watchOS) 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 }