Files
Reflect/Shared/Services/WatchConnectivityManager.swift
Trey t e5656f47fd Rename iFeels to Feels across entire codebase
- Bundle IDs: com.tt.ifeel* → com.tt.feels*
- App Groups: group.com.tt.ifeel* → group.com.tt.feels*
- iCloud containers: iCloud.com.tt.ifeel* → iCloud.com.tt.feels*
- IAP product IDs: com.tt.ifeel.IAP.* → com.tt.feels.IAP.*
- URLs: ifeels.app → feels.app
- Logger subsystems and dispatch queues
- Product names and display names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:57:44 -06:00

167 lines
5.1 KiB
Swift

//
// 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
}