Enable CloudKit sync for Watch app and fix WCSession handling
- Watch now uses CloudKit for data persistence (syncs automatically with iPhone) - Added iCloud/CloudKit entitlements to Watch app (debug and release) - Fixed WCSession delegate to handle messages with reply handler - Watch UI now shows "Already Rated" screen after voting - Invalidate LiveActivityScheduler cache when mood is logged - WCSession now used only for immediate UI updates, not data persistence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
// Feels
|
||||
//
|
||||
// Central coordinator for Watch Connectivity.
|
||||
// iOS app is the hub - all mood logging flows through here.
|
||||
// Used for immediate UI updates (Live Activity, widget refresh).
|
||||
// Data persistence is handled by CloudKit sync.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -12,7 +13,7 @@ import WidgetKit
|
||||
import os.log
|
||||
|
||||
/// Manages Watch Connectivity between iOS and watchOS
|
||||
/// iOS app acts as the central coordinator for all mood logging
|
||||
/// Used for immediate notifications, not data persistence (CloudKit handles that)
|
||||
final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||
|
||||
static let shared = WatchConnectivityManager()
|
||||
@@ -21,6 +22,11 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||
|
||||
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
|
||||
@@ -33,7 +39,7 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||
session = WCSession.default
|
||||
session?.delegate = self
|
||||
session?.activate()
|
||||
Self.logger.info("WCSession activated")
|
||||
Self.logger.info("WCSession activation requested")
|
||||
} else {
|
||||
Self.logger.warning("WCSession not supported on this device")
|
||||
}
|
||||
@@ -59,12 +65,20 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||
// 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
|
||||
/// 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,
|
||||
session.activationState == .activated else {
|
||||
Self.logger.warning("WCSession not ready")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -74,9 +88,17 @@ final class WatchConnectivityManager: NSObject, ObservableObject {
|
||||
"date": date.timeIntervalSince1970
|
||||
]
|
||||
|
||||
// Use transferUserInfo for guaranteed delivery
|
||||
session.transferUserInfo(message)
|
||||
Self.logger.info("Sent mood \(mood) to iPhone for logging")
|
||||
// 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
|
||||
@@ -91,6 +113,16 @@ extension WatchConnectivityManager: WCSessionDelegate {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,17 +136,27 @@ extension WatchConnectivityManager: WCSessionDelegate {
|
||||
session.activate()
|
||||
}
|
||||
|
||||
// iOS receives mood from watch and logs it centrally
|
||||
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]) {
|
||||
guard let action = message["action"] as? String else { return }
|
||||
guard let action = message["action"] as? String else {
|
||||
Self.logger.error("No action in message")
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "logMood":
|
||||
@@ -126,26 +168,25 @@ extension WatchConnectivityManager: WCSessionDelegate {
|
||||
}
|
||||
|
||||
let date = Date(timeIntervalSince1970: timestamp)
|
||||
Self.logger.info("Received mood \(moodRaw) from watch, logging centrally")
|
||||
Self.logger.info("Processing mood \(moodRaw) from watch for \(date)")
|
||||
|
||||
Task { @MainActor in
|
||||
// Use MoodLogger for centralized logging with all side effects
|
||||
MoodLogger.shared.logMood(mood, for: date, entryType: .watch)
|
||||
}
|
||||
|
||||
case "reloadWidgets":
|
||||
Self.logger.info("Received reloadWidgets from watch")
|
||||
Task { @MainActor in
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
Self.logger.warning("Unknown action: \(action)")
|
||||
}
|
||||
}
|
||||
#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")
|
||||
|
||||
Reference in New Issue
Block a user