1 contributor
348 lines | 9.741kb

CloudKit Sync - Troubleshooting & Debugging

Debugging Checklist

1. Verify CloudKit Account Status

// In AppDelegate
private func logCloudKitStatus() {
    let container = CKContainer(identifier: "iCloud.ro.xdev.USB-Meter")
    container.accountStatus { status, error in
        if let error {
            print("CloudKit error: \(error.localizedDescription)")
            return
        }
        switch status {
        case .available:
            print("✅ CloudKit available")
        case .noAccount:
            print("❌ No iCloud account")
        case .restricted:
            print("❌ CloudKit restricted (parental controls?)")
        case .couldNotDetermine:
            print("❌ Status unknown")
        case .temporarilyUnavailable:
            print("⚠️ CloudKit temporarily unavailable")
        @unknown default:
            print("⚠️ Unknown status")
        }
    }
}

Expected: .available or changes won't sync


2. Check Entitlements

Verify USB Meter.entitlements has:

<key>com.apple.developer.icloud-container-identifiers</key>
<array>
    <string>iCloud.ro.xdev.USB-Meter</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
    <string>CloudKit</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
    <string>iCloud.ro.xdev.USB-Meter</string>
</array>

If missing: - ❌ App can't access CloudKit - ❌ No sync errors in logs (silent failure)


3. Monitor Sync Logs

Enable enhanced Core Data debugging:

In Xcode scheme editor: 1. Select scheme → Edit Scheme → Run → Arguments 2. Add Launch Arguments: -com.apple.CoreData.SQLDebug 1 -com.apple.CoreData.ConcurrencyDebug 1

In terminal: ```bash

Follow app logs

log stream --predicate 'process == "USB Meter"' --level debug ```

Look for: - NSPersistentCloudKitContainer initialization messages - cloudkit: messages (sync operations) - CoreData: messages (database operations)


4. Check Persistent Store Description

// In AppDelegate.persistentContainer
if let description = container.persistentStoreDescriptions.first {
    print("Store URL: \(description.url?.path ?? "nil")")
    print("Options: \(description.options ?? [:])")
    print("CloudKit container: \(description.cloudKitContainerOptions?.containerIdentifier ?? "nil")")
}

Expected: - URL should point to .../Library/Application Support/CKModel.sqlite - CloudKit container identifier must match entitlements


5. Verify Remote Notifications Are Set Up

// In AppData
private func setupRemoteChangeNotificationObserver() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(remoteStoreDidChange),
        name: .NSPersistentStoreRemoteChange,
        object: nil
    )
    print("✅ Remote change observer registered")
}

@objc private func remoteStoreDidChange() {
    print("🔄 Remote CloudKit change received")
    DispatchQueue.main.async {
        self.reloadSettingsFromCloudStore()
    }
}

Problem: If reloadSettingsFromCloudStore is never called, check if observer is registered


Common Issues & Fixes

Issue 1: "Changes not syncing to other devices"

Diagnosis: ```swift // Check if save succeeded do { try appData.persistentContainer.viewContext.save() print("✅ Save successful") } catch { print("❌ Save failed: (error)") }

// Check if DeviceSettings record exists let request = NSFetchRequest(entityName: "DeviceSettings") let count = try? appData.persistentContainer.viewContext.count(for: request) print("Records in store: (count ?? 0)") ```

Fixes: 1. Check Account Status → must be .available 2. Check Merge Policy → ensure NSMergeByPropertyStoreTrumpMergePolicy 3. Check ObserverremoteStoreDidChange registered? 4. Force Sync: swift appData.persistentContainer.viewContext.refreshAllObjects() appData.reloadSettingsFromCloudStore()

Issue 2: "Duplicates appearing in CloudKit"

Diagnosis: ```swift // Check for MAC address duplicates let request = NSFetchRequest(entityName: "DeviceSettings") request.returnsDistinctResults = true request.returnsObjectsAsFaults = false request.resultType = .dictionaryResultType request.returnsDistinctResults = true

let macs = try? appData.persistentContainer.viewContext.fetch(request) .compactMap { $0["macAddress"] as? String }

let duplicates = macs?.filter { mac in macs?.filter { $0 == mac }.count ?? 0 > 1 } print("Duplicate MACs: (duplicates ?? [])") ```

Fixes: 1. Rebuild v3: Ensure cloudStoreRebuildVersion = 3 is deployed 2. Check rebuildCanonicalStoreIfNeeded: swift // Manually trigger appData.cloudStore?.rebuildCanonicalStoreIfNeeded(version: 3) 3. Verify UserDefaults: swift let rebuilt = UserDefaults.standard.bool(forKey: "cloudStoreRebuildVersion.3") print("Rebuild v3 completed: \(rebuilt)")

Issue 3: "Connection status stuck as 'Connected on Device X'"

Diagnosis: ```swift // Check connection record let request = NSFetchRequest(entityName: "DeviceSettings") let records = try? appData.persistentContainer.viewContext.fetch(request)

for record in records ?? [] { if let expiry = record.value(forKey: "connectedExpiryAt") as? Date { let isExpired = expiry < Date.now print("✋ MAC (record.value(forKey: "macAddress") ?? "?") expires at (expiry), expired: (isExpired)") } } ```

Fixes: 1. Manually clear lock: swift appData.clearMeterConnection(macAddress: "aa:bb:cc:dd:ee:ff") 2. Check TTL hardcode: Should be 120s in setConnection() 3. Verify system clock on Device A (didn't fall asleep or drift)

Issue 4: "Core Data save errors" with NSError 1569

Symptom: NSError Domain=NSCocoaErrorDomain Code=1569 "The object's persistent store is not reachable"

Causes: 1. ❌ Old model with uniquenessConstraint conflicting with CloudKit 2. ❌ All attributes on DeviceSettings aren't optional="YES"

Fixes: 1. Verify model v2+: Check .xccurrentversion points to USB_Meter 2.xcdatamodel 2. Check attributes: All must be optional="YES" (inspect .xcdatamodel/contents) 3. Reset store if persists: swift // Delete local store to force rebuild from CloudKit let storeURL = appData.persistentContainer.persistentStoreDescriptions.first?.url let fileManager = FileManager.default try? fileManager.removeItem(at: storeURL)


Monitoring CloudKit Sync in Real Time

Dashboard View (for debugging)

struct CloudKitDebugView: View {
    @ObservedObject var appData: AppData

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("CloudKit Sync Status")
                .font(.headline)

            DebugItem(label: "Account", value: appData.cloudKitAccountStatus)
            DebugItem(label: "Device ID", value: UIDevice.current.identifierForVendor?.uuidString ?? "?")
            DebugItem(label: "Device Name", value: UIDevice.current.name)
            DebugItem(label: "Last Sync", value: formatDate(appData.lastSyncTime))
            DebugItem(label: "Records", value: "\(appData.deviceSettingsCount)")
            DebugItem(label: "Pending", value: "\(appData.pendingSyncCount)")

            Button("Force Reload") {
                appData.reloadSettingsFromCloudStore()
            }
        }
        .padding()
        .font(.caption)
    }
}

struct DebugItem: View {
    let label: String
    let value: String

    var body: some View {
        HStack {
            Text(label).font(.caption2).foregroundColor(.secondary)
            Spacer()
            Text(value).font(.caption).monospaced()
        }
    }
}

Network Conditions Testing

Simulate CloudKit Unavailability

  1. Xcode → Debug → Simulate Location → [Custom]
  2. Instrument → Network Link Conditioner → "Very Bad Network"

Expected Behavior:

  • ✅ Local changes proceed (saved to local DB)
  • ✅ Notification badge appears: "📶 Waiting for sync…"
  • ✅ When network returns → auto-resumes sync

CloudKit Container Inspection (Advanced)

Using CloudKit Console (requires Apple developer account):

  1. Go to https://icloud.developer.apple.com/cloudkit
  2. Select container: iCloud.ro.xdev.USB-Meter
  3. Browse DeviceSettings records:
    • Check for duplicates by macAddress
    • Verify connectedByDeviceID matches expected device UUIDs
    • Check updatedAt timestamps

Performance Profiling

// Measure reloadSettingsFromCloudStore() performance
let start = Date()
appData.reloadSettingsFromCloudStore()
let duration = Date().timeIntervalSince(start)
print("Reload took \(duration * 1000)ms")  // Should be <100ms for ~10 records

Benchmarks: | Operation | Target | Actual (good) | |-----------|--------|---------------| | setConnection() | <50ms | ~10ms | | reloadSettingsFromCloudStore() | <100ms | ~30ms (5 records) | | CloudKit push (network) | <5s | varies by connection | | Remote notification delivery | <10s | ~2-5s (typically) |


Recording Logs for Bug Reports

# Save full debug logs
log collect --start '2025-03-26 10:00:00' --output /tmp/cloudkit_logs.logarchive

# Extract readable format
log show /tmp/cloudkit_logs.logarchive \
  --predicate 'process == "USB Meter"' \
  --level debug > ~/cloudkit_debug.txt

Include in bug report: 1. ~/cloudkit_debug.txt 2. Reproduction steps 3. What you expected vs. what happened 4. Device names & OS versions involved