← Back to index

WatchEntryStore

Spots Watch App
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import Combine import CoreLocation import WatchConnectivity import WidgetKitFramework importsImports Foundation, Combine, CoreLocation, WatchConnectivity, WidgetKit.
▶ STORE
class WatchEntryStore: ObservableObject { @Published var entries: [WatchEntry] = [] @Published var isLoaded: Bool = false @Published var userLocation: CLLocation? private let storageKey = "watchEntries" private let appGroupID = "group.com.spots.shared" private let sessionDelegate = WatchSessionDelegate() private let locationManager = WatchLocationManager() init() { loadLocal() locationManager.store = self sessionDelegate.store = self if WCSession.isSupported() { WCSession.default.delegate = sessionDelegate WCSession.default.activate() } else { print("⚠️ Watch: WCSession not supported") } } // MARK: - Local persistence func loadLocal() { defer { isLoaded = true } guard let data = UserDefaults.standard.data(forKey: storageKey) else { print("ℹ️ Watch: no cached entries found") return } do { let saved = try JSONDecoder().decode([WatchEntry].self, from: data) entries = saved print("ℹ️ Watch: loaded \(saved.count) entry(s) from cache") } catch { print("⚠️ Watch: cache decode failed — \(error). Clearing stale cache.") UserDefaults.standard.removeObject(forKey: storageKey) } } func saveLocal() { guard let data = try? JSONEncoder().encode(entries) else { return } UserDefaults.standard.set(data, forKey: storageKey) UserDefaults(suiteName: appGroupID)?.set(data, forKey: storageKey) WidgetCenter.shared.reloadAllTimelines() print("ℹ️ Watch: saved \(entries.count) entry(s) to cache") } /// Manually request entries from iPhone — call from UI for a user-triggered refresh. /// Returns true if a request was actually sent (iPhone is reachable), false if not. @discardableResult func requestSync() -> Bool { guard WCSession.default.activationState == .activated else { return false } if WCSession.default.isReachable { WCSession.default.sendMessage( ["request": "entries"], replyHandler: nil, errorHandler: { err in print("⚠️ Watch manual sync failed: \(err)") } ) print("ℹ️ Watch: sent sync request to iPhone") return true } else { // Not reachable right now — sessionReachabilityDidChange will auto-request // as soon as the iPhone app comes to the foreground. print("ℹ️ Watch: iPhone not reachable — will auto-sync when Spots opens on iPhone") return false } } func startLocationIfNeeded() { locationManager.startIfNeeded() } func apply(received: [WatchEntry]) { DispatchQueue.main.async { self.entries = received self.saveLocal() } } }`WatchEntryStore` classDefines the `WatchEntryStore` class. Conforms to ObservableObject.
▶ WCSESSIONDELEGATE
class WatchSessionDelegate: NSObject, WCSessionDelegate { weak var store: WatchEntryStore? func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { if let error { print("⚠️ Watch WCSession error: \(error)") return } print("ℹ️ Watch WCSession activated — reachable: \(session.isReachable)") // Apply any application context that arrived while the app wasn't running if let data = session.receivedApplicationContext["entries"] as? Data { do { let received = try JSONDecoder().decode([WatchEntry].self, from: data) print("ℹ️ Watch: loaded \(received.count) entry(s) from receivedApplicationContext") store?.apply(received: received) } catch { print("⚠️ Watch: receivedApplicationContext decode failed — \(error)") } } if session.isReachable { requestEntriesFromPhone(session) } } /// Called when iPhone app opens/closes while Watch app is running. func sessionReachabilityDidChange(_ session: WCSession) { print("ℹ️ Watch: reachability changed — isReachable: \(session.isReachable)") if session.isReachable { requestEntriesFromPhone(session) } } private func requestEntriesFromPhone(_ session: WCSession) { session.sendMessage( ["request": "entries"], replyHandler: nil, errorHandler: { err in print("⚠️ Watch sendMessage failed: \(err)") } ) } // Primary delivery path — application context (latest-wins, no queue stalling) func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { print("ℹ️ Watch: received application context") guard let data = applicationContext["entries"] as? Data else { print("⚠️ Watch: application context missing 'entries' key") return } do { let received = try JSONDecoder().decode([WatchEntry].self, from: data) print("ℹ️ Watch: decoded \(received.count) entry(s) from application context") store?.apply(received: received) } catch { print("⚠️ Watch: application context decode failed — \(error)") } } func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) { print("ℹ️ Watch: received userInfo keys: \(userInfo.keys.joined(separator: ", "))") guard let data = userInfo["entries"] as? Data else { print("⚠️ Watch: userInfo missing 'entries' key") return } do { let received = try JSONDecoder().decode([WatchEntry].self, from: data) print("ℹ️ Watch: decoded \(received.count) entry(s) from transferUserInfo") store?.apply(received: received) } catch { print("⚠️ Watch: decode failed — \(error)") } } func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { print("ℹ️ Watch: received message keys: \(message.keys.joined(separator: ", "))") guard let data = message["entries"] as? Data else { return } do { let received = try JSONDecoder().decode([WatchEntry].self, from: data) print("ℹ️ Watch: decoded \(received.count) entry(s) from message") store?.apply(received: received) } catch { print("⚠️ Watch: message decode failed — \(error)") } } }`WatchSessionDelegate` classDefines the `WatchSessionDelegate` class. Conforms to NSObject, WCSessionDelegate.
▶ LOCATION MANAGER
class WatchLocationManager: NSObject, CLLocationManagerDelegate { weak var store: WatchEntryStore? private let manager = CLLocationManager() private var started = false override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyHundredMeters } /// Call this lazily when the user first requests Nearby sort. func startIfNeeded() { guard !started else { return } started = true switch manager.authorizationStatus { case .notDetermined: manager.requestWhenInUseAuthorization() case .authorizedWhenInUse, .authorizedAlways: manager.startUpdatingLocation() default: break } } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: manager.startUpdatingLocation() default: break } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } DispatchQueue.main.async { self.store?.userLocation = location } } }`WatchLocationManager` classDefines the `WatchLocationManager` class. Conforms to NSObject, CLLocationManagerDelegate.