# Platform Sync (iOS + watchOS) This document describes the end-to-end WatchConnectivity workflow used with LocalData. It covers the responsibilities of the LocalData package versus app-level wiring, and shows how to build a launch-order-safe sync handshake. ## Goals - Sync selected keys between iOS and watchOS using `WCSession.updateApplicationContext`. - Avoid launch-order dependencies (either app can start first). - Keep business data stored in LocalData; the watch app displays data in memory. ## What LocalData Provides LocalData does **not** activate `WCSession` for you. It provides the core sync utilities: - `SyncHelper.syncIfNeeded(...)` sends `applicationContext` for eligible keys. - `StorageRouter.syncRegisteredKeysIfNeeded()` re-sends stored values for all registered keys that are eligible for sync. - `StorageRouter.syncSnapshot()` builds a payload snapshot (key → Data) to reply to watch requests. These APIs are intentionally low-level so apps can decide when to sync (launch, reachability, user actions, etc.). ## What the App Must Provide Your app owns the WatchConnectivity lifecycle: 1. **Activate WCSession** on iOS and watch. 2. **Bootstrap sync** at app launch and on watch availability changes. 3. **Handle watch-initiated requests** and reply with a snapshot. 4. **Decode payloads** on watch and update UI state. ## Recommended Architecture ### iOS App - Create a `WatchConnectivityService` (or similar) that: - Activates `WCSession`. - Calls `StorageRouter.syncRegisteredKeysIfNeeded()` on: - `activationDidComplete` - `sessionWatchStateDidChange` - `sessionReachabilityDidChange` - Handles watch messages: - `didReceiveMessage(_:replyHandler:)` → reply with `StorageRouter.syncSnapshot()` - `didReceiveUserInfo` → trigger `syncRegisteredKeysIfNeeded()` for queued requests ### watchOS App - Create a `WatchConnectivityService` that: - Activates `WCSession`. - Requests sync on launch or reachability changes: - If `reachable`: `sendMessage` with a `request_sync` key and use the reply payload. - If not reachable: `transferUserInfo` with the same request key (queued until iOS launches). - Loads `applicationContext` on activation and decodes known keys. - Create lightweight handlers (e.g., `UserProfileWatchHandler`) to decode payloads into in-memory state. ## Launch-Order-Safe Handshake This prevents the "watch first" or "phone first" problem. 1. **Watch launches first** - Sends `request_sync` via `transferUserInfo` if iPhone unreachable. - When iPhone launches, it receives the queued request and replies with a snapshot. 2. **iPhone launches first** - Bootstraps by calling `syncRegisteredKeysIfNeeded()`. - When the watch launches, it requests a sync and receives a snapshot immediately. Either order works with this handshake. ## Data Eligibility Rules A key is eligible for sync when: - `availability` is `.all` or `.phoneWithWatchSync` - `syncPolicy` is `.manual` or `.automaticSmall` - For `.automaticSmall`, the payload must be <= `SyncConfiguration.maxAutoSyncSize` Keys with `.never` will not sync unless your app explicitly overrides behavior. ## Example: iOS Service ```swift @MainActor final class WatchConnectivityService: NSObject, WCSessionDelegate { static let shared = WatchConnectivityService() private override init() { super.init() let session = WCSession.default session.delegate = self session.activate() } func session(_ session: WCSession, activationDidCompleteWith: WCSessionActivationState, error: Error?) { Task { await StorageRouter.shared.syncRegisteredKeysIfNeeded() } } func sessionWatchStateDidChange(_ session: WCSession) { Task { await StorageRouter.shared.syncRegisteredKeysIfNeeded() } } func sessionReachabilityDidChange(_ session: WCSession) { Task { await StorageRouter.shared.syncRegisteredKeysIfNeeded() } } func session( _ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void ) { Task { let snapshot = await StorageRouter.shared.syncSnapshot() replyHandler(snapshot.isEmpty ? ["ack": true] : snapshot) await StorageRouter.shared.syncRegisteredKeysIfNeeded() } } } ``` ## Example: watch Service ```swift @MainActor final class WatchConnectivityService: NSObject, WCSessionDelegate { static let shared = WatchConnectivityService() private override init() { super.init() let session = WCSession.default session.delegate = self session.activate() } func session(_ session: WCSession, activationDidCompleteWith: WCSessionActivationState, error: Error?) { requestSyncIfNeeded() } func sessionReachabilityDidChange(_ session: WCSession) { requestSyncIfNeeded() } private func requestSyncIfNeeded() { let session = WCSession.default if session.isReachable { session.sendMessage(["request_sync": true]) { reply in // Apply payload } } else { session.transferUserInfo(["request_sync": true]) } } } ``` ## Common Pitfalls - **Simulator unreliability**: WatchConnectivity is inconsistent in simulators. Test on real devices. - **Missing entitlements**: Watch app must be embedded and signed correctly. - **Launch order assumptions**: Always use the handshake; do not assume which app starts first. - **Large payloads**: Automatic sync will fail if the payload exceeds the configured max size. ## SecureStorageSample Reference For a working implementation, see: - iOS: `SecureStorageSample/SecureStorageSample/Services/WatchConnectivityService.swift` - Watch: `SecureStorageSample/SecureStorageSample Watch App/Services/WatchConnectivityService.swift` - Watch handlers: `SecureStorageSample/SecureStorageSample Watch App/Services/Handlers/` - Bootstrap: `SecureStorageSample/SecureStorageSample/SecureStorageSampleApp.swift`