Running Days Docs
GitHub

iOS Sync

The iOS app syncs workouts to the Running Days API for cross-device access and web dashboard.

Sync Architecture

text
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  HealthKit  │ ───▶ │   iOS App   │ ───▶ │  Cloud API  │
└─────────────┘      └─────────────┘      └─────────────┘

                     Local Cache
  1. HealthKit provides workout data
  2. iOS App transforms and caches locally
  3. Cloud API stores for web dashboard access

API Endpoint

http
POST /api/v1/workouts/sync
Authorization: Bearer <token>

Request Body:

json
{
  "workouts": [
    {
      "client_id": "healthkit_abc123",
      "start_time": "2024-01-15T08:00:00Z",
      "end_time": "2024-01-15T08:35:00Z",
      "duration_seconds": 2100,
      "distance_meters": 5000,
      "energy_burned_kcal": 350,
      "avg_heart_rate": 145,
      "source": "apple_watch"
    }
  ],
  "mode": "incremental",
  "idempotency_key": "sync_20240115_abc",
  "client_sync_timestamp": "2024-01-15T09:00:00Z"
}

Response:

json
{
  "success": true,
  "sync_id": "sync_xyz789",
  "server_timestamp": "2024-01-15T09:00:01Z",
  "created": 1,
  "updated": 0,
  "unchanged": 0,
  "conflicts": []
}

Sync Modes

Incremental (Default)

Only syncs workouts since last successful sync.

swift
let lastSync = UserDefaults.standard.object(forKey: "lastSyncDate") as? Date
let newWorkouts = try await healthKit.fetchWorkouts(since: lastSync ?? .distantPast)

Full

Re-syncs all workouts. Used for:

  • First-time sync
  • Recovery after data corruption
  • Manual refresh by user

Conflict Resolution

When the same workout exists on client and server:

ScenarioResolution
Identical dataSkip (unchanged)
Different data, same IDServer wins (update local)
Client has newer workoutCreate on server

Idempotency

Each sync request includes an idempotency key to handle network retries:

swift
let idempotencyKey = "sync_\(Date().timeIntervalSince1970)_\(UUID().uuidString.prefix(8))"

If the same key is sent twice, the server returns the cached response with header:

text
X-Idempotent-Replayed: true

Offline Support

Workouts are cached locally using SwiftData:

swift
@Model
class CachedWorkout {
    var id: String
    var data: Data
    var syncedAt: Date?
    var needsSync: Bool
}

When offline:

  1. New workouts are saved with needsSync = true
  2. On reconnect, pending workouts are synced
  3. After successful sync, needsSync = false

Error Handling

ErrorRecovery
401 UnauthorizedRefresh token and retry
429 Rate LimitedExponential backoff
500 Server ErrorRetry with backoff
Network OfflineQueue for later

Sync Status UI

Show sync status to users:

swift
enum SyncStatus {
    case idle
    case syncing(progress: Double)
    case success(count: Int)
    case error(message: String)
}