Docs PlatformSync, README
Summary: - Docs: PlatformSync, README - Added symbols: class WatchConnectivityService, func session, func sessionWatchStateDidChange, func sessionReachabilityDidChange, func requestSyncIfNeeded Stats: - 2 files changed, 190 insertions(+)
This commit is contained in:
parent
358e7a0ffc
commit
fb90270d9f
168
Documentation/PlatformSync.md
Normal file
168
Documentation/PlatformSync.md
Normal file
@ -0,0 +1,168 @@
|
||||
# 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`
|
||||
|
||||
22
README.md
22
README.md
@ -418,6 +418,28 @@ StorageRouter can sync data to Apple Watch via WCSession when:
|
||||
|
||||
The app owns WCSession activation and handling incoming updates.
|
||||
|
||||
### Bootstrapping on Launch
|
||||
To ensure the watch receives the latest values after app relaunch or reconnection, call:
|
||||
|
||||
```swift
|
||||
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
||||
```
|
||||
|
||||
This re-sends stored values for any registered keys that are eligible for sync. Apps typically call this on launch and when WatchConnectivity becomes reachable.
|
||||
|
||||
### Responding to Watch-Initiated Sync Requests
|
||||
If your watch app asks for an explicit refresh, you can build a snapshot of syncable data and reply via WCSession messaging:
|
||||
|
||||
```swift
|
||||
let snapshot = await StorageRouter.shared.syncSnapshot()
|
||||
// Reply with snapshot (key: Data) via WCSession.sendMessage
|
||||
```
|
||||
|
||||
### Platform Sync Guide
|
||||
For end-to-end iOS + watchOS setup (including a launch-order-safe handshake), see:
|
||||
|
||||
`Documentation/PlatformSync.md`
|
||||
|
||||
## Platforms
|
||||
- iOS 17+
|
||||
- watchOS 10+
|
||||
|
||||
Loading…
Reference in New Issue
Block a user