← Back to index

PhoneConnectivityManager

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import WatchConnectivityFramework importsImports Foundation, WatchConnectivity.
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` classDefines the `PhoneConnectivityManager` class. Conforms to NSObject, WCSessionDelegate.
extension Notification.Name { static let watchRequestedEntries = Notification.Name("watchRequestedEntries") static let wcSessionActivated = Notification.Name("wcSessionActivated") }`Notification` extensionDefines the `Notification` extension.