Mecanismul de replicare a datelor către iCloud.
CKModel.xcdatamodeld (curent: USB_Meter 2)iCloud.ro.xdev.USB-MeterUSB_Meter 1 (original)
↓
USB_Meter 2 (fix: removed uniqueness constraints)
↓
USB_Meter 3? (future)
Curent: USB_Meter 2 (schema v20)
| Field | Type | Notes |
|---|---|---|
id |
UUID | Primary key |
macAddress |
String | Optional (for migration) |
meterName |
String | Chosen by user |
tc66TemperatureUnit |
String | "celsius" / "fahrenheit" |
createdAt |
Date | Immutable |
updatedAt |
Date | Last sync timestamp |
connectionMetadata |
JSON blob | Device, timestamp, expiry |
discoveryMetadata |
JSON blob | Last seen, seen by |
cloudKitRecordID |
String | Reference toward CloudKit |
macAddress nu are uniquenessConstraint (not CloudKit safe)macAddress optional, duplicate entries ar trebui mergedmeterName e unic per meter (no machine-generated names)updatedAt se schimbă la fiecare syncconnectionMetadata.expiresAt = now() + 24hUser changes meterName
↓
AppData calls cloudStore.upsertDeviceSettings(...)
↓
NSManagedObjectContext saves
↓
NSPersistentCloudKitContainer auto-uploads to CloudKit
↓
CloudKit replica updated
↓
Other devices see change via NSPersistentCloudKitContainerEventChangeNotification
iCloud change appears
↓
NSPersistentCloudKitContainer notifies
↓
AppData observes NSPersistentCloudKitContainerEventChangeNotification
↓
Merge remote change with local state
↓
Core Data context updated
↓
UI refreshes
Scenario 1: Same meter renamed on two devices simultaneously
Device A: "Kitchen Meter" → "Main Meter" (10:00:00)
Device B: "Kitchen Meter" → "Lab Meter" (10:00:05)
Resolution: Last timestamp wins → "Lab Meter" (Device B @ 10:00:05)
Scenario 2: Duplicate entries cu aceeași MAC
Device A: macAddress = "AA:BB:CC:DD:EE:FF", meterName = "Meter 1"
Device B (after restore): macAddress = "AA:BB:CC:DD:EE:FF", meterName = "Meter 1"
Resolution: Merge duplicate entries, keep one record (last write wins)
func recordDiscovery(for macAddress: String) {
let lastSeen = discoveryMetadata[macAddress]?.lastSeen
let now = Date()
if now.timeIntervalSince(lastSeen ?? .distantPast) >= 120 {
// OK: permit sync
cloudStore.recordDiscovery(macAddress, discoveredAt: now)
}
// else: skip (under throttle window)
}
Issue (March 2025):
- Old uniquenessConstraint pe macAddress = incompatibil cu CloudKit
- Old rebuild logic: delete ALL + recreate = delete storm în CloudKit
- Old naming: meterName = platform model ("iPad") = irelevant pe CloudKit
Schema migration: USB_Meter 1 → USB_Meter 2
- Removed uniquenessConstraint
- Made macAddress optional
- Changed device name = hostname (all platforms)
Rebuild refactoring: rebuildCanonicalStoreIfNeeded()
```swift
func rebuildCanonicalStoreIfNeeded(newVersion: Int) {
if cloudStoreRebuildVersion >= newVersion { return }
// Update winner in-place, delete only duplicates
let groupedByMAC = groupEntries(by: \.macAddress)
for (mac, entries) in groupedByMAC {
guard entries.count > 1 else { continue }
let winner = entries.max(by: \.updatedAt)
let losers = entries.filter { $0.id != winner.id }
// Delete losers only, keep winner
for loser in losers {
context.delete(loser)
}
}
cloudStoreRebuildVersion = newVersion
try? context.save()
} ```
cloudStoreRebuildVersion = 3 după fix// Legacy stores
let meterNames = NSUbiquitousKeyValueStore.default.dictionary(forKey: "MeterNames")
// → {macAddress → meterName}
let tempUnits = NSUbiquitousKeyValueStore.default.dictionary(forKey: "TC66TemperatureUnits")
// → {macAddress → unit}
swift
cloudStore.upsertDeviceSettings(
macAddress: mac,
meterName: meterNames[mac],
tc66TemperatureUnit: tempUnits[mac]
)
Mark as migrated în defaults
test_cloudStoreSaves_ToNSManagedObjectContext()
test_upsertDeviceSettings_CreatesOrUpdates()
test_discoveryThrottling_RespectsTiming()
test_conflictResolution_LastWriteWins()
test_rebuildCanonicalStore_UpdatesWinner_DeletesDuplicates()
test_macAddressNoDuplicates_AfterRebuild()
Error: Network unavailable
→ Fail fast (don't retry immediately)
→ Queue pending changes in Core Data
→ Retry at next network change (observe URLSession events)
Error: Quote exceeded (too many records)
→ Log error
→ Notify user: "iCloud storage full"
→ Option: Archive old charge records
Error: Conflict detected
→ Auto-resolve via "last write wins"
→ Log both versions for debugging
→ Notify user if significant loss
NSPersistentCloudKitContainer: from CloudKit frameworkCloudDeviceSettingsStore: wrapper in AppData.swiftCKModel.xcdatamodeld: Core Data schemaNSUbiquitousKeyValueStore: legacy (deprecated)