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.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.