Why Watch + Phone Flows Fail
Cross-device features break when each side “owns” a different truth. The right model is one canonical session state, two device-specific projections.
Step 1: Define a canonical workout session snapshot
struct WorkoutSnapshot: Codable {
let sessionId: String
let startedAt: Date
let elapsedSeconds: Int
let currentRound: Int
let isPaused: Bool
}
Step 2: Centralize message transport behind one gateway
final class DeviceSyncGateway: NSObject {
private let session = WCSession.default
func send(snapshot: WorkoutSnapshot) throws {
let data = try JSONEncoder().encode(snapshot)
try session.updateApplicationContext(["snapshot": data])
}
}
Step 3: Resolve conflicts by timestamp + monotonic counter
When both devices send updates, choose the newest valid state using a deterministic rule, not ad-hoc if-statements.
func shouldReplace(current: WorkoutSnapshot, incoming: WorkoutSnapshot) -> Bool {
if incoming.elapsedSeconds != current.elapsedSeconds {
return incoming.elapsedSeconds > current.elapsedSeconds
}
return incoming.currentRound >= current.currentRound
}
Step 4: Persist last valid snapshot locally
If one device restarts mid-session, restore from disk first, then reconcile with remote.
Pitfalls
- Sending full UI state trees over WatchConnectivity.
- Ignoring delayed delivery behavior.
- No reconciliation strategy after reconnect.
Verification
- Session survives app restart on both devices.
- Pause/resume state converges after temporary disconnection.
- No duplicate rounds in final workout summary.