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- HealthKit provides workout data
- iOS App transforms and caches locally
- 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:
| Scenario | Resolution |
|---|---|
| Identical data | Skip (unchanged) |
| Different data, same ID | Server wins (update local) |
| Client has newer workout | Create 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: trueOffline 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:
- New workouts are saved with
needsSync = true - On reconnect, pending workouts are synced
- After successful sync,
needsSync = false
Error Handling
| Error | Recovery |
|---|---|
| 401 Unauthorized | Refresh token and retry |
| 429 Rate Limited | Exponential backoff |
| 500 Server Error | Retry with backoff |
| Network Offline | Queue 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)
}