125 lines
4.7 KiB
Swift
125 lines
4.7 KiB
Swift
import Foundation
|
|
import LocalData
|
|
import SharedKit
|
|
import WatchConnectivity
|
|
|
|
@MainActor
|
|
/// iOS-side WatchConnectivity bridge that keeps the watch app in sync with LocalData.
|
|
/// This class focuses on coordination and delegates data payload creation to LocalData.
|
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|
static let shared = WatchConnectivityService()
|
|
|
|
private override init() {
|
|
super.init()
|
|
activateIfSupported()
|
|
}
|
|
|
|
/// Activates WCSession only on supported devices (no-ops on unsupported hardware).
|
|
private func activateIfSupported() {
|
|
guard WCSession.isSupported() else { return }
|
|
let session = WCSession.default
|
|
session.delegate = self
|
|
session.activate()
|
|
}
|
|
|
|
func session(
|
|
_ session: WCSession,
|
|
activationDidCompleteWith activationState: WCSessionActivationState,
|
|
error: Error?
|
|
) {
|
|
if let error {
|
|
Logger.error("iOS WCSession activation failed", error: error)
|
|
} else {
|
|
Logger.debug("iOS WCSession activated with state: \(activationState.rawValue)")
|
|
}
|
|
// Try to sync any previously registered keys as soon as WCSession is ready.
|
|
Task {
|
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
|
}
|
|
}
|
|
|
|
func sessionDidBecomeInactive(_ session: WCSession) {
|
|
// No-op; required for iOS WCSessionDelegate.
|
|
}
|
|
|
|
func sessionDidDeactivate(_ session: WCSession) {
|
|
session.activate()
|
|
}
|
|
|
|
func sessionWatchStateDidChange(_ session: WCSession) {
|
|
Logger.debug("iOS WCSession watch state changed: paired=\(session.isPaired) installed=\(session.isWatchAppInstalled)")
|
|
// A watch install or pairing change is a good time to re-send syncable keys.
|
|
Task {
|
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
|
}
|
|
}
|
|
|
|
func sessionReachabilityDidChange(_ session: WCSession) {
|
|
Logger.debug("iOS WCSession reachability changed: reachable=\(session.isReachable)")
|
|
// When reachability flips, attempt to push any pending sync payloads.
|
|
Task {
|
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
|
}
|
|
}
|
|
|
|
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
|
// Watch may request a sync without needing a reply (fire-and-forget message).
|
|
if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
|
Logger.debug("iOS received watch sync request")
|
|
Task {
|
|
let snapshot = await StorageRouter.shared.syncSnapshot()
|
|
if snapshot.isEmpty {
|
|
Logger.debug("iOS sync snapshot empty; falling back to application context")
|
|
}
|
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
|
|
func session(
|
|
_ session: WCSession,
|
|
didReceiveMessage message: [String: Any],
|
|
replyHandler: @escaping ([String: Any]) -> Void
|
|
) {
|
|
// Reply-based handshake: watch expects a payload immediately if reachable.
|
|
if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
|
Logger.debug("iOS received watch sync request (reply)")
|
|
Task {
|
|
let payload = await buildSyncReplyPayload()
|
|
replyHandler(payload)
|
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attempts to build a snapshot payload, retrying briefly in case keys are still initializing.
|
|
private func buildSyncReplyPayload() async -> [String: Any] {
|
|
let maxAttempts = 3
|
|
for attempt in 1...maxAttempts {
|
|
let snapshot = await StorageRouter.shared.syncSnapshot()
|
|
if !snapshot.isEmpty {
|
|
Logger.debug("iOS sync reply snapshot ready on attempt \(attempt)")
|
|
return snapshot
|
|
}
|
|
|
|
if attempt < maxAttempts {
|
|
Logger.debug("iOS sync reply snapshot empty; retrying (\(attempt))")
|
|
try? await Task.sleep(for: .milliseconds(300))
|
|
}
|
|
}
|
|
|
|
Logger.debug("iOS sync reply snapshot empty after retries; replying with ack only")
|
|
return ["ack": true]
|
|
}
|
|
|
|
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
|
// transferUserInfo can deliver requests when the watch was previously unreachable.
|
|
if let request = userInfo[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
|
Logger.debug("iOS received queued watch sync request")
|
|
Task {
|
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
}
|