Summary: - Docs: PlatformSync, README - Added symbols: class WatchConnectivityService, func session, func sessionWatchStateDidChange, func sessionReachabilityDidChange, func requestSyncIfNeeded Stats: - 2 files changed, 190 insertions(+)
169 lines
6.0 KiB
Markdown
169 lines
6.0 KiB
Markdown
# 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`
|
|
|