learning_ai_clock/ios/ChronoMindWatch/WatchSessionManager.swift
saravanakumardb1 d179c4c624 feat(watch,mac): complete watchOS + macOS companion targets
- watchOS: add WatchSessionManager (WCSession bridge), WatchNotificationHandler
  (snooze/dismiss actions), recommendations() for AppIntentTimelineProvider
- watchOS: WatchTimerStore tick loop with cascade haptics, App Group sync
- macOS: CreateTimerSheet (countdown/alarm/pomodoro), launch-at-login toggle
- macOS: MenuBarState showCreateSheet, MacSettingsView data tab
- Xcode project updated with new file references
2026-03-27 11:28:13 -07:00

167 lines
5.6 KiB
Swift

// WCSession manager for bidirectional iPhone Watch sync
import Foundation
import WatchConnectivity
import os
// MARK: - Watch Timer Command
enum WatchTimerCommand: Codable {
case snooze(id: String, minutes: Int)
case dismiss(id: String)
case pause(id: String)
case resume(id: String)
case complete(id: String)
var messageDict: [String: Any] {
switch self {
case .snooze(let id, let minutes):
return ["command": "snooze", "id": id, "minutes": minutes]
case .dismiss(let id):
return ["command": "dismiss", "id": id]
case .pause(let id):
return ["command": "pause", "id": id]
case .resume(let id):
return ["command": "resume", "id": id]
case .complete(let id):
return ["command": "complete", "id": id]
}
}
}
// MARK: - Watch Session Manager
@MainActor
final class WatchSessionManager: NSObject, ObservableObject {
static let shared = WatchSessionManager()
@Published var isReachable = false
private let session: WCSession
private let logger = Logger(subsystem: "com.chronomind.watch", category: "WatchSessionManager")
private var pendingCommands: [[String: Any]] = []
private override init() {
session = WCSession.default
super.init()
}
// MARK: - Activation
func activate() {
guard WCSession.isSupported() else {
logger.warning("WCSession not supported on this device")
return
}
session.delegate = self
session.activate()
logger.info("WCSession activation requested")
}
// MARK: - Send Command
func sendCommand(_ command: WatchTimerCommand) {
let message = command.messageDict
guard session.activationState == .activated else {
logger.warning("Session not activated, queuing command")
pendingCommands.append(message)
return
}
if session.isReachable {
session.sendMessage(message, replyHandler: { reply in
Task { @MainActor in
self.logger.info("Command acknowledged: \(reply.description)")
}
}, errorHandler: { error in
Task { @MainActor in
self.logger.error("Failed to send command: \(error.localizedDescription)")
self.pendingCommands.append(message)
}
})
} else {
// Fallback: use application context for non-urgent delivery
do {
try session.updateApplicationContext(["pendingCommand": message])
logger.info("Command sent via application context")
} catch {
logger.error("Failed to update application context: \(error.localizedDescription)")
pendingCommands.append(message)
}
}
}
// MARK: - Flush Pending
private func flushPendingCommands() {
guard session.isReachable, !pendingCommands.isEmpty else { return }
let commands = pendingCommands
pendingCommands.removeAll()
for command in commands {
session.sendMessage(command, replyHandler: nil) { [weak self] error in
Task { @MainActor in
self?.logger.error("Failed to flush command: \(error.localizedDescription)")
}
}
}
logger.info("Flushed \(commands.count) pending commands")
}
}
// MARK: - WCSessionDelegate
extension WatchSessionManager: WCSessionDelegate {
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
Task { @MainActor in
if let error = error {
logger.error("WCSession activation failed: \(error.localizedDescription)")
return
}
logger.info("WCSession activated: \(activationState.rawValue)")
isReachable = session.isReachable
flushPendingCommands()
}
}
nonisolated func sessionReachabilityDidChange(_ session: WCSession) {
Task { @MainActor in
isReachable = session.isReachable
logger.info("Reachability changed: \(session.isReachable)")
if session.isReachable {
flushPendingCommands()
}
}
}
nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
Task { @MainActor in
logger.info("Received application context update")
// iPhone pushed updated timer data refresh from App Group
NotificationCenter.default.post(name: .watchTimerDataUpdated, object: nil)
}
}
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
Task { @MainActor in
logger.info("Received message: \(message.keys.joined(separator: ", "))")
// iPhone pushed a real-time update refresh from App Group
NotificationCenter.default.post(name: .watchTimerDataUpdated, object: nil)
}
}
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
Task { @MainActor in
logger.info("Received message with reply: \(message.keys.joined(separator: ", "))")
NotificationCenter.default.post(name: .watchTimerDataUpdated, object: nil)
replyHandler(["status": "received"])
}
}
}
// MARK: - Notification Name
extension Notification.Name {
static let watchTimerDataUpdated = Notification.Name("watchTimerDataUpdated")
}