Newer Older
348 lines | 9.74kb
Bogdan Timofte authored a week ago
1
# CloudKit Sync - Troubleshooting & Debugging
2

            
3
## Debugging Checklist
4

            
5
### 1. Verify CloudKit Account Status
6
```swift
7
// In AppDelegate
8
private func logCloudKitStatus() {
9
    let container = CKContainer(identifier: "iCloud.ro.xdev.USB-Meter")
10
    container.accountStatus { status, error in
11
        if let error {
12
            print("CloudKit error: \(error.localizedDescription)")
13
            return
14
        }
15
        switch status {
16
        case .available:
17
            print("✅ CloudKit available")
18
        case .noAccount:
19
            print("❌ No iCloud account")
20
        case .restricted:
21
            print("❌ CloudKit restricted (parental controls?)")
22
        case .couldNotDetermine:
23
            print("❌ Status unknown")
24
        case .temporarilyUnavailable:
25
            print("⚠️ CloudKit temporarily unavailable")
26
        @unknown default:
27
            print("⚠️ Unknown status")
28
        }
29
    }
30
}
31
```
32

            
33
**Expected:** `.available` or changes won't sync
34

            
35
---
36

            
37
### 2. Check Entitlements
38
Verify `USB Meter.entitlements` has:
39

            
40
```xml
41
<key>com.apple.developer.icloud-container-identifiers</key>
42
<array>
43
    <string>iCloud.ro.xdev.USB-Meter</string>
44
</array>
45
<key>com.apple.developer.icloud-services</key>
46
<array>
47
    <string>CloudKit</string>
48
</array>
49
<key>com.apple.developer.ubiquity-container-identifiers</key>
50
<array>
51
    <string>iCloud.ro.xdev.USB-Meter</string>
52
</array>
53
```
54

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

            
59
---
60

            
61
### 3. Monitor Sync Logs
62

            
63
Enable enhanced Core Data debugging:
64

            
65
**In Xcode scheme editor:**
66
1. Select scheme → Edit Scheme → Run → Arguments
67
2. Add Launch Arguments:
68
   ```
69
   -com.apple.CoreData.SQLDebug 1
70
   -com.apple.CoreData.ConcurrencyDebug 1
71
   ```
72

            
73
**In terminal:**
74
```bash
75
# Follow app logs
76
log stream --predicate 'process == "USB Meter"' --level debug
77
```
78

            
79
**Look for:**
80
- `NSPersistentCloudKitContainer` initialization messages
81
- `cloudkit:` messages (sync operations)
82
- `CoreData:` messages (database operations)
83

            
84
---
85

            
86
### 4. Check Persistent Store Description
87

            
88
```swift
89
// In AppDelegate.persistentContainer
90
if let description = container.persistentStoreDescriptions.first {
91
    print("Store URL: \(description.url?.path ?? "nil")")
92
    print("Options: \(description.options ?? [:])")
93
    print("CloudKit container: \(description.cloudKitContainerOptions?.containerIdentifier ?? "nil")")
94
}
95
```
96

            
97
**Expected:**
98
- URL should point to `.../Library/Application Support/CKModel.sqlite`
99
- CloudKit container identifier must match entitlements
100

            
101
---
102

            
103
### 5. Verify Remote Notifications Are Set Up
104

            
105
```swift
106
// In AppData
107
private func setupRemoteChangeNotificationObserver() {
108
    NotificationCenter.default.addObserver(
109
        self,
110
        selector: #selector(remoteStoreDidChange),
111
        name: .NSPersistentStoreRemoteChange,
112
        object: nil
113
    )
114
    print("✅ Remote change observer registered")
115
}
116

            
117
@objc private func remoteStoreDidChange() {
118
    print("🔄 Remote CloudKit change received")
119
    DispatchQueue.main.async {
120
        self.reloadSettingsFromCloudStore()
121
    }
122
}
123
```
124

            
125
**Problem:** If `reloadSettingsFromCloudStore` is never called, check if observer is registered
126

            
127
---
128

            
129
## Common Issues & Fixes
130

            
131
### Issue 1: "Changes not syncing to other devices"
132

            
133
**Diagnosis:**
134
```swift
135
// Check if save succeeded
136
do {
137
    try appData.persistentContainer.viewContext.save()
138
    print("✅ Save successful")
139
} catch {
140
    print("❌ Save failed: \(error)")
141
}
142

            
143
// Check if DeviceSettings record exists
144
let request = NSFetchRequest<NSFetchRequestExpression>(entityName: "DeviceSettings")
145
let count = try? appData.persistentContainer.viewContext.count(for: request)
146
print("Records in store: \(count ?? 0)")
147
```
148

            
149
**Fixes:**
150
1. **Check Account Status** → must be `.available`
151
2. **Check Merge Policy** → ensure `NSMergeByPropertyStoreTrumpMergePolicy`
152
3. **Check Observer** → `remoteStoreDidChange` registered?
153
4. **Force Sync:**
154
   ```swift
155
   appData.persistentContainer.viewContext.refreshAllObjects()
156
   appData.reloadSettingsFromCloudStore()
157
   ```
158

            
159
### Issue 2: "Duplicates appearing in CloudKit"
160

            
161
**Diagnosis:**
162
```swift
163
// Check for MAC address duplicates
164
let request = NSFetchRequest<NSDictionary>(entityName: "DeviceSettings")
165
request.returnsDistinctResults = true
166
request.returnsObjectsAsFaults = false
167
request.resultType = .dictionaryResultType
168
request.returnsDistinctResults = true
169

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

            
173
let duplicates = macs?.filter { mac in
174
    macs?.filter { $0 == mac }.count ?? 0 > 1
175
}
176
print("Duplicate MACs: \(duplicates ?? [])")
177
```
178

            
179
**Fixes:**
180
1. **Rebuild v3:** Ensure `cloudStoreRebuildVersion = 3` is deployed
181
2. **Check `rebuildCanonicalStoreIfNeeded`:**
182
   ```swift
183
   // Manually trigger
184
   appData.cloudStore?.rebuildCanonicalStoreIfNeeded(version: 3)
185
   ```
186
3. **Verify UserDefaults:**
187
   ```swift
188
   let rebuilt = UserDefaults.standard.bool(forKey: "cloudStoreRebuildVersion.3")
189
   print("Rebuild v3 completed: \(rebuilt)")
190
   ```
191

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

            
194
**Diagnosis:**
195
```swift
196
// Check connection record
197
let request = NSFetchRequest<NSManagedObject>(entityName: "DeviceSettings")
198
let records = try? appData.persistentContainer.viewContext.fetch(request)
199

            
200
for record in records ?? [] {
201
    if let expiry = record.value(forKey: "connectedExpiryAt") as? Date {
202
        let isExpired = expiry < Date.now
203
        print("✋ MAC \(record.value(forKey: "macAddress") ?? "?") expires at \(expiry), expired: \(isExpired)")
204
    }
205
}
206
```
207

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

            
216
### Issue 4: "Core Data save errors" with `NSError 1569`
217

            
218
**Symptom:**
219
```
220
NSError Domain=NSCocoaErrorDomain Code=1569
221
"The object's persistent store is not reachable"
222
```
223

            
224
**Causes:**
225
1. ❌ Old model with `uniquenessConstraint` conflicting with CloudKit
226
2. ❌ All attributes on DeviceSettings aren't `optional="YES"`
227

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

            
239
---
240

            
241
## Monitoring CloudKit Sync in Real Time
242

            
243
### Dashboard View (for debugging)
244

            
245
```swift
246
struct CloudKitDebugView: View {
247
    @ObservedObject var appData: AppData
248

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

            
254
            DebugItem(label: "Account", value: appData.cloudKitAccountStatus)
255
            DebugItem(label: "Device ID", value: UIDevice.current.identifierForVendor?.uuidString ?? "?")
Bogdan Timofte authored a week ago
256
            DebugItem(label: "Device Name", value: AppData.myDeviceName)
Bogdan Timofte authored a week ago
257
            DebugItem(label: "Last Sync", value: formatDate(appData.lastSyncTime))
258
            DebugItem(label: "Records", value: "\(appData.deviceSettingsCount)")
259
            DebugItem(label: "Pending", value: "\(appData.pendingSyncCount)")
260

            
261
            Button("Force Reload") {
262
                appData.reloadSettingsFromCloudStore()
263
            }
264
        }
265
        .padding()
266
        .font(.caption)
267
    }
268
}
269

            
270
struct DebugItem: View {
271
    let label: String
272
    let value: String
273

            
274
    var body: some View {
275
        HStack {
276
            Text(label).font(.caption2).foregroundColor(.secondary)
277
            Spacer()
278
            Text(value).font(.caption).monospaced()
279
        }
280
    }
281
}
282
```
283

            
284
---
285

            
286
## Network Conditions Testing
287

            
288
### Simulate CloudKit Unavailability
289
1. Xcode → Debug → Simulate Location → [Custom]
290
2. Instrument → Network Link Conditioner → "Very Bad Network"
291

            
292
### Expected Behavior:
293
- ✅ Local changes proceed (saved to local DB)
294
- ✅ Notification badge appears: "📶 Waiting for sync…"
295
- ✅ When network returns → auto-resumes sync
296

            
297
---
298

            
299
## CloudKit Container Inspection (Advanced)
300

            
301
Using CloudKit Console (requires Apple developer account):
302

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

            
310
---
311

            
312
## Performance Profiling
313

            
314
```swift
315
// Measure reloadSettingsFromCloudStore() performance
316
let start = Date()
317
appData.reloadSettingsFromCloudStore()
318
let duration = Date().timeIntervalSince(start)
319
print("Reload took \(duration * 1000)ms")  // Should be <100ms for ~10 records
320
```
321

            
322
**Benchmarks:**
323
| Operation | Target | Actual (good) |
324
|-----------|--------|---------------|
325
| `setConnection()` | <50ms | ~10ms |
326
| `reloadSettingsFromCloudStore()` | <100ms | ~30ms (5 records) |
327
| CloudKit push (network) | <5s | varies by connection |
328
| Remote notification delivery | <10s | ~2-5s (typically) |
329

            
330
---
331

            
332
## Recording Logs for Bug Reports
333

            
334
```bash
335
# Save full debug logs
336
log collect --start '2025-03-26 10:00:00' --output /tmp/cloudkit_logs.logarchive
337

            
338
# Extract readable format
339
log show /tmp/cloudkit_logs.logarchive \
340
  --predicate 'process == "USB Meter"' \
341
  --level debug > ~/cloudkit_debug.txt
342
```
343

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