| Code | What It Does | How It Does It |
| ▶ IMPORTS | | |
| import Foundation
import Combine
import CoreLocation
import WatchConnectivity
import WidgetKit | Framework imports | Imports 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` class | Defines 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` class | Defines 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` class | Defines the `WatchLocationManager` class. Conforms to NSObject, CLLocationManagerDelegate. |