| class PhoneConnectivityManager: NSObject, WCSessionDelegate {
static let shared = PhoneConnectivityManager()
/// Set by ContentView on launch so the manager can reply to watch requests
/// immediately without going through the notification chain.
var entriesProvider: (() -> [SpotEntry])?
/// Last payload sent to the Watch — used to suppress redundant transfers.
private var lastSentData: Data?
private override init() {
super.init()
// Intentionally empty — WCSession activation is expensive (mach_msg2_trap
// blocks for up to 8 s on the calling thread). Call activateWCSession()
// from a background thread after the singleton is created.
}
/// Activates WatchConnectivity. Must be called from a background thread —
/// WCSession.activate() makes a synchronous Mach IPC call to the Watch
/// daemon that can block for 8+ seconds if the daemon is slow to respond.
func activateWCSession() {
guard WCSession.isSupported() else { return }
WCSession.default.delegate = self
WCSession.default.activate()
}
/// Call this whenever the entry list changes. Only transmits when the
/// Watch-relevant data (WatchEntry fields) actually changed — suppresses
/// spurious transfers caused by image updates, in-progress typing, etc.
func sendEntries(_ entries: [SpotEntry], force: Bool = false) {
guard WCSession.default.activationState == .activated,
WCSession.default.isPaired,
WCSession.default.isWatchAppInstalled else { return }
let watchEntries = entries.map { $0.watchEntry }
guard let data = try? JSONEncoder().encode(watchEntries) else { return }
guard force || data != lastSentData else { return }
lastSentData = data
// updateApplicationContext replaces any pending update immediately —
// no queue stalling, always delivers the latest snapshot.
do {
try WCSession.default.updateApplicationContext(["entries": data])
print("ℹ️ Updated application context (\(watchEntries.count) entries, force=\(force))")
} catch {
print("⚠️ updateApplicationContext failed: \(error)")
}
}
// MARK: - WCSessionDelegate
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {
if let error {
print("⚠️ WCSession activation error: \(error)")
} else if activationState == .activated {
// iPhone WCSession is ready — notify ContentView to push entries immediately
DispatchQueue.main.async {
NotificationCenter.default.post(name: .wcSessionActivated, object: nil)
}
}
}
func sessionDidBecomeInactive(_ session: WCSession) {}
/// Called when the Watch app opens/closes while the iPhone app is running.
/// Push entries immediately so the Watch gets fresh data without having to request.
func sessionWatchStateDidChange(_ session: WCSession) {
guard session.isPaired, session.isWatchAppInstalled else { return }
print("ℹ️ Watch state changed — isReachable: \(session.isReachable), counterpartAppInstalled: \(session.isWatchAppInstalled)")
if session.isReachable {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .wcSessionActivated, object: nil)
}
}
}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}
// Watch requesting a fresh sync — reply immediately via sendMessage for low latency.
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
guard message["request"] as? String == "entries" else { return }
replyToWatch(session: session)
}
func session(_ session: WCSession, didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void) {
guard message["request"] as? String == "entries" else { replyHandler([:]); return }
replyToWatch(session: session)
replyHandler([:])
}
/// Encodes current entries and sends them to the watch immediately via sendMessage,
/// falling back to transferUserInfo if the watch is not currently reachable.
private func replyToWatch(session: WCSession) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let entries = self.entriesProvider?() ?? []
let watchEntries = entries.map { $0.watchEntry }
guard let data = try? JSONEncoder().encode(watchEntries) else { return }
self.lastSentData = data
// Always update the application context so the watch gets the data
// even if the sendMessage below fails or the watch isn't reachable.
try? session.updateApplicationContext(["entries": data])
if session.isReachable {
session.sendMessage(
["entries": data],
replyHandler: nil,
errorHandler: { err in
print("⚠️ sendMessage to watch failed: \(err) — context already updated")
}
)
print("ℹ️ Replied to watch via sendMessage + context (\(watchEntries.count) entries)")
} else {
print("ℹ️ Replied to watch via context only (\(watchEntries.count) entries)")
}
}
}
} | `PhoneConnectivityManager` class | Defines the `PhoneConnectivityManager` class. Conforms to NSObject, WCSessionDelegate. |