1 contributor
167 lines | 7.978kb

CloudKit Sync Schema

Overview

USB Meter utilizează NSPersistentCloudKitContainer pentru a sincroniza setările dispozitivelor Bluetooth pe mai multe device-uri iOS/iPadOS via iCloud CloudKit.

Container CloudKit: iCloud.ro.xdev.USB-Meter


Core Data Entity: DeviceSettings

Purpose

Stocare centralizată a setărilor și metadatelor de conexiune pentru fiecare contor Bluetooth, cu sincronizare automată pe toate device-urile utilizatorului.

Device Name Source

  • All platforms (iOS, iPadOS, macOS Catalyst, iPad on Mac): ProcessInfo.processInfo.hostName
    • Examples: "iPhone-User", "MacBook-Pro.local", "iPad-WiFi"
    • Rationale: Platform-agnostic, avoids device model names ("iPad", "iPhone" are useless)

Attributes

Atribut Tip Optional Descriere
macAddress String YES Adresa MAC unică a contorului BT (cheie candidat pentru deduplicare)
meterName String YES Renumire personalizată a contorului (ex: "Kitchen Meter")
tc66TemperatureUnit String YES Preferință unitate temperatură pentru TC66C (raw value enum: "celsius"/"fahrenheit")
modelType String YES Tip model contor ("UM25C", "UM34C", "TC66C")
connectedByDeviceID String YES UUID-ul device-ului iOS care l-a conectat ultima dată
connectedByDeviceName String YES Numele device-ului iOS care l-a conectat
connectedAt Date YES Timestamp al celei mai recente conexiuni
connectedExpiryAt Date YES TTL pentru lock-ul de conexiune (alt device poate prelua dacă a expirat)
lastSeenAt Date YES Timestamp al ultimei avertisment BT din device
lastSeenByDeviceID String YES UUID device-ul iOS care l-a văzut ultima dată
lastSeenByDeviceName String YES Nume device-ul iOS care l-a văzut
lastSeenPeripheralName String YES Numele periferalei așa cum raporta dispozitivul
updatedAt Date YES Timestamp al ultimei actualizări (audit trail)

CloudKit Mapping

Fiecare atribut Core Data → câmp CloudKit în record-ul DeviceSettings: - Sincronizare automată bidirecțională - Lightweight migration pentru schimbări de schemă - Conflict resolution: NSMergeByPropertyStoreTrumpMergePolicy (CloudKit wins pe changed properties)


Constraints

NOT Used (v1 - v2):

  • uniquenessConstraint pe macAddressincompatibil cu NSPersistentCloudKitContainer (genereaza erori silentioase)

✅ Applied:

  • Logical deduplication per macAddress în rebuildCanonicalStoreIfNeeded
    • Ruleaza o dată pe device (verifică UserDefaults cu key "cloudStoreRebuildVersion.{n}")
    • Merges conflicting records pe baza timestamp-urilor

Data Flow

┌─────────────────────────────────────────────────────────────────┐
│                      Device A (iPhone)                          │
├─────────────────────────────────────────────────────────────────┤
│  User connects UM25C (MAC: aa:bb:cc:dd:ee:ff)                  │
│         ↓                                                        │
│  Meter.swift → appData.publishMeterConnection(mac, type)       │
│         ↓                                                        │
│  CloudDeviceSettingsStore.setConnection()                      │
│    - Creates / Updates DeviceSettings record                   │
│    - Sets connectedByDeviceID = Device A UUID                 │
│    - connectedAt = now                                         │
│    - connectedExpiryAt = now + 120s                           │
│         ↓                                                        │
│  NSPersistentCloudKitContainer saves to Core Data              │
│         ↓                                                        │
│  CloudKit framework (automatic sync):                          │
│    - Pushes record modificare → iCloud server                  │
│    - Records: [CKRecord with RecordID "DeviceSettings/mac"]   │
│         ↓                                                        │
│  iCloud Server → Device B (iPad)                               │
│         ↓                                                        │
│  Device B receives remote change notification                  │
│  (NSPersistentStoreRemoteChangeNotificationPostOptionKey)      │
│         ↓                                                        │
│  AppData.reloadSettingsFromCloudStore()                        │
│    - Fetches updated record din local Core Data                │
│    - Sees connectedByDeviceID = Device A UUID                 │
│    - Checks connectedExpiryAt (not expired)                   │
│    - Knows someone else is connected                          │
│         ↓                                                        │
│  UI updates: shows "Connected on iPhone"                       │
└─────────────────────────────────────────────────────────────────┘

Conflict Resolution Scenarios

Scenario 1: Simultaneous Connection Attempt

Device A și Device B conectează același contor ~același timp.

Device A:                          Device B:
t=0.0s: setConnection()           t=0.1s: setConnection()
  - connectedByDeviceID = A         - connectedByDeviceID = B
  - connectedAt = 0.0s              - connectedAt = 0.1s
       ↓ save                             ↓ save
  CloudKit pushes A's record    CloudKit pushes B's record
       ↓                              ↓
       └──────→ iCloud Server ←──────┘
               (concurrent writes)

Merge Strategy:
  - NSMergeByPropertyStoreTrumpMergePolicy
  - Latest CloudKit record "wins" per property
  - Result: B's connectedByDeviceID + connectedAt win
    (mai recent timestamp)

Scenario 2: Reconnection After Expiry

Device A conectează la t=0. Expiry la t=120. Device B incearcă conexiune la t=150.

t=0s:   Device A connects (connectedAt=0, expiry=120)
t=120s: Connection expires
t=150s: Device B checks:
        if (connectedExpiryAt < now):
          // Lock expired, can connect
        setConnection() → Device B becomes new owner

Deduplication Logic (v3+)

Rulează la AppDelegate.didFinishLaunchingWithOptions:

rebuildCanonicalStoreIfNeeded(version: 3)
  1. Fetch ALL DeviceSettings records
  2. Group by normalized macAddress
  3. Per group:
     - Select "winner" (prefer: more recent updatedAt, more data fields populated)
     - Merge other records INTO winner
     - Delete duplicates (only delete-uri, no inserts)
  4. Save context → CloudKit deletes propagate
  5. Mark in UserDefaults as "done for v3"

De ce in-place? Pentru a nu cauza "delete storm": - ❌ Old way: Delete ALL + Insert ALL → alt device primește DELETE-uri înainte de INSERT-uri → pierde date - ✅ New way: Update IN-PLACE + Delete only duplicates → record ID-ul DeviceSettings este păstrat → sincronizare progresivă


Version History

Model Version Changes
USB_Meter.xcdatamodel 1 Original schema cu uniquenessConstraint
USB_Meter 2.xcdatamodel 2 ❌ Removed constraint (but rebuild was destructive)
USB_Meter 2.xcdatamodel 3 ✅ Rebuilt with in-place merge strategy

Migration path: v1 → v2 (lightweight, auto) → v3 (rebuild in-place, UserDefaults gated)