1 contributor
365 lines | 18.16kb

CloudKit Sync - Visual Reference

System Architecture Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                           USB Meter App                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────────────┐         ┌─────────────────────────────────┐  │
│  │ Meter.swift      │         │ BluetoothManager.swift          │  │
│  │  (UI + State)    │◄────────│  (BT Communication)            │  │
│  └────────┬─────────┘         └─────────────────────────────────┘  │
│           │                                                         │
│           │ operationalState changes                               │
│           │ (connect/disconnect)                                   │
│           ▼                                                         │
│  ┌────────────────────────────────────────────────────────────┐   │
│  │           AppData                                          │   │
│  │  (Global Sync Orchestrator)                               │   │
│  ├────────────────────────────────────────────────────────────┤   │
│  │ • publishMeterConnection(mac, type)                       │   │
│  │ • clearMeterConnection(mac)                               │   │
│  │ • reloadSettingsFromCloudStore()                          │   │
│  │ • setupRemoteChangeNotificationObserver()                 │   │
│  └────────┬──────────────────────────┬──────────────────────┘   │
│           │                          │                           │
│           ▼                          ▼                           │
│  ┌────────────────┐        ┌────────────────────────┐           │
│  │ CloudDeviceSettings  │        │ NSManagedObjectContext │           │
│  │ Store                │        │ (viewContext)          │           │
│  │ (CR operations)      │        └────────────────────────┘           │
│  └────────┬──────┘                                                │
│           │                                                        │
│           ▼                                                        │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │   NSPersistentCloudKitContainer                         │    │
│  │  (Automatic sync layer)                                │    │
│  ├─────────────────────────────────────────────────────────┤    │
│  │ • Detects changes                                      │    │
│  │ • Queues for CloudKit                                  │    │
│  │ • Receives remote notifications                        │    │
│  │ • Applies merge policy                                 │    │
│  └──────────────┬──────────────────────┬────────────────┘    │
│                │                      │                      │
└────────────────┼──────────────────────┼──────────────────────┘
                │                      │
                ▼                      ▼
        ┌───────────────────┐  ┌──────────────────────┐
        │  Local SQLite DB  │  │  Persistent History  │
        │  (DeviceSettings) │  │  Journal             │
        └───────────────────┘  └──────────────────────┘
                │                      │
                └──────────┬───────────┘
                           │
                ┌──────────▼────────────┐
                │  CloudKit Framework   │
                │  (Device background   │
                │   process)            │
                └──────────┬────────────┘
                           │
                    [Network Sync]
                           │
                ┌──────────▼────────────┐
                │   iCloud Servers      │
                │   CloudKit Database   │
                └──────────┬────────────┘
                           │
                ┌──────────▼──────────────────┐
                │  Other Devices (iPhone,     │
                │  iPad, Mac) on same         │
                │  iCloud account             │
                └─────────────────────────────┘

Local Write Flow (Sequence Diagram)

Device A              AppData              Core Data          CloudKit
  │                    │                      │                  │
  │─ User connects ───►│                      │                  │
  │ meter              │                      │                  │
  │                    │─ setConnection() ───►│                  │
  │                    │ (foreground QoS)     │                  │
  │                    │                      │                  │
  │◄─ UI Updates      │◄─ @Published change  │                  │
  │ (local)            │   (immediate)        │                  │
  │                    │                      │                  │
  │                    │─ context.save() ────►│                  │
  │                    │                      │                  │
  │                    │                      │─ Detect change   │
  │                    │                      │─ Create CKRecord │
  │                    │                      │─ Queue for sync ─►│
  │                    │                      │                  │─ Network upload
  │                    │                      │                  │ (1-2s)
  │                    │                      │                  │
  ┼────────────────────┼──────────────────────┼──────────────────┼───► [iCloud Server]
  │                    │                      │                  │
  │    (Device B receives remote change notification after ~5s)   │

Remote Change Reception (Sequence Diagram)

Device B           System iOS            Core Data          AppData
  │                  │                      │                  │
  │               [iCloud notifies]         │                  │
  │                  │                      │                  │
  │                  │─ Remote change ─────►│                  │
  │                  │ notification posted  │                  │
  │                  │                      │                  │
  │                  │                      │─ Merge changes   │
  │                  │                      │ (merge policy)  │
  │                  │                      │─ Save locally   │
  │                  │                      │                  │
  │                  │─ NSPersistentStore ─►│─ remoteStoreDidChange()
  │                  │  RemoteChange        │                  │
  │                  │  notification        │                  │
  │                  │                      │                  │
  │                  │                      │◄─ reloadSettings()
  │                  │                      │ FromCloudStore()
  │◄─ UI updates ◄───────────────────────────────────────────────
  │ "Connected on                           │
  │  Device A"                              │

Connection Lock Lifecycle

t=0s:  Device A connects
       ┌─────────────────────────────────────┐
       │ connectedByDeviceID = UUID_A        │
       │ connectedByDeviceName = "iPhone A"  │
       │ connectedAt = 2025-03-26 10:00:00   │
       │ connectedExpiryAt = 10:02:00        │ ◄─ TTL = 120s
       └─────────────────────────────────────┘
                │
                │ [CloudKit syncs]
                ▼
       [Device B sees connection]
       "Meter locked by iPhone A"

t=60s: Device A still connected
       (TTL still valid)

t=120s: Connection expires
       ┌─────────────────────────────────────┐
       │ connectedExpiryAt = 10:02:00         │
       │ now() > connectedExpiryAt ✓          │
       └─────────────────────────────────────┘
                │
       [Device B checks expiry]
                │
       if (now > connectedExpiryAt)
           Device B can connect!

Merge Conflict Resolution

Device A                          Device B
t=0.5s: Sets meterName           t=0.6s: Sets meterName
  "Kitchen Meter"                  "Charger Meter"
  │                                │
  ▼                                ▼
  Save & Queue                     Save & Queue
  │                                │
  └───────────┬────────────────────┘
              ▼
        [Both pushed to CloudKit]
        (Race condition)

CloudKit Merge Policy:
  NSMergeByPropertyStoreTrumpMergePolicy
  → Latest write wins

Result on both devices:
  ✗ One value reverts (~1s after cloud update)
  ✓ No data loss, eventually consistent

Rebuild Flow (v2 → v3)

User launches app (v3)
  │
  ▼
AppDelegate.didFinishLaunchingWithOptions
  │
  ▼
rebuildCanonicalStoreIfNeeded(version: 3)
  │
  ├─ Check UserDefaults["cloudStoreRebuildVersion.3"]
  │
  ├─ if already done:
  │   return (skip)
  │
  └─ if not done:
      │
      ├─ Fetch all DeviceSettings records
      │
      ├─ Group by normalized macAddress
      │
      ├─ For each group:
      │   ├─ Find "winner" (most data + most recent)
      │   └─ Merge others INTO winner
      │   └─ Delete duplicates
      │
      ├─ context.save()
      │   (CloudKit propagates deletes)
      │
      ├─ UserDefaults["cloudStoreRebuildVersion.3"] = true
      │
      └─ ✓ Done (won't run again)

State Machine: Meter Connection

┌─────────────────┐
│     OFFLINE     │  ◄──────────────────────────────┐
└────────┬────────┘                                 │
         │                                          │
    advertisment heard
         │
         ▼
┌─────────────────────────────┐
│   PERIPHERAL_NOT_CONNECTED  │
└────────┬────────────────────┘
         │
    user clicks "Connect"
    Meter.connect() called
         │
         ▼
┌─────────────────────────────┐
│  PERIPHERAL_CONNECTION_PD   │
└────────┬────────────────────┘
         │
    BT services discovered
         │
         ▼
┌─────────────────────────────┐
│   PERIPHERAL_CONNECTED      │
│   (appData.publishMeterConn)│  ◄─────── CloudKit SyncStart
└────────┬────────────────────┘
         │
    peripheral ready for commands
         │
         ▼
┌─────────────────────────────┐
│   PERIPHERAL_READY          │
└────────┬────────────────────┘
         │
    data request sent
         │
         ▼
┌─────────────────────────────┐
│   COMMUNICATING             │
└────────┬────────────────────┘
         │
    data received
         │
         ▼
┌─────────────────────────────┐
│   DATA_IS_AVAILABLE         │ ──────── Cloud Polling Loop
└─────────────────────────────┘           (every 60s renew connection)
         │
    connect lost / timeout
         │
         ▼ (Or clear in UI)
    OFFLINE again

Connection Renewal (Heartbeat)

┌─────────────────────────────┐
│ Meter.connect() called      │
│ operationalState changed    │
│ → .peripheralConnected      │
└──────────┬──────────────────┘
           │
           ▼
       startConnectionRenewal()
           │
           ├─ Create timer: interval = 60s
           │
           └─ repeats every 60s:
              │
              ├─ appData.publishMeterConnection()
              │  (updates connectedAt + connectedExpiryAt)
              │
              └─ CloudKit syncs (keeps lock alive)

Disconnection:
┌─────────────────────────────┐
│ operationalState changed    │
│ → .offline / .notConnected  │
└──────────┬──────────────────┘
           │
           ▼
       stopConnectionRenewal()
           │
           ├─ connectionRenewalTimer.invalidate()
           │
           └─ appData.clearMeterConnection()
              (removes connectedByDeviceID)
              │
              └─ CloudKit syncs (lock released)

Error Recovery Paths

┌─────────────────────┐
│ Normal Operation    │
└─────────────────────┘
         │
         ▼ [Error: CloudKit unavailable]
┌──────────────────────────────┐
│ Queue changes locally         │
│ (stored in SQLite)           │
└──────────────────────────────┘
         │
         ▼ [Network restored]
┌──────────────────────────────┐
│ NSPersistentCloudKit         │
│ resumes sync automatically    │
└──────────────────────────────┘
         │
         ▼
┌─────────────────────┐
│ Back to normal      │
└─────────────────────┘

┌─────────────────────┐
│ Collision detected  │ (Device A & B both edited same field)
└─────────────────────┘
         │
         ▼
┌──────────────────────────────┐
│ NSMergeByPropertyStoreTrump   │
│ (CloudKit version wins)       │
└──────────────────────────────┘
         │
         └─ User sees local change revert ~1s after
              cloud update arrives (rare, acceptable)