Newer Older
365 lines | 18.16kb
Bogdan Timofte authored a week ago
1
# CloudKit Sync - Visual Reference
2

            
3
## System Architecture Diagram
4

            
5
```
6
┌─────────────────────────────────────────────────────────────────────┐
7
│                           USB Meter App                             │
8
├─────────────────────────────────────────────────────────────────────┤
9
│                                                                     │
10
│  ┌──────────────────┐         ┌─────────────────────────────────┐  │
11
│  │ Meter.swift      │         │ BluetoothManager.swift          │  │
12
│  │  (UI + State)    │◄────────│  (BT Communication)            │  │
13
│  └────────┬─────────┘         └─────────────────────────────────┘  │
14
│           │                                                         │
15
│           │ operationalState changes                               │
16
│           │ (connect/disconnect)                                   │
17
│           ▼                                                         │
18
│  ┌────────────────────────────────────────────────────────────┐   │
19
│  │           AppData                                          │   │
20
│  │  (Global Sync Orchestrator)                               │   │
21
│  ├────────────────────────────────────────────────────────────┤   │
22
│  │ • publishMeterConnection(mac, type)                       │   │
23
│  │ • clearMeterConnection(mac)                               │   │
24
│  │ • reloadSettingsFromCloudStore()                          │   │
25
│  │ • setupRemoteChangeNotificationObserver()                 │   │
26
│  └────────┬──────────────────────────┬──────────────────────┘   │
27
│           │                          │                           │
28
│           ▼                          ▼                           │
29
│  ┌────────────────┐        ┌────────────────────────┐           │
30
│  │ CloudDeviceSettings  │        │ NSManagedObjectContext │           │
31
│  │ Store                │        │ (viewContext)          │           │
32
│  │ (CR operations)      │        └────────────────────────┘           │
33
│  └────────┬──────┘                                                │
34
│           │                                                        │
35
│           ▼                                                        │
36
│  ┌─────────────────────────────────────────────────────────┐    │
37
│  │   NSPersistentCloudKitContainer                         │    │
38
│  │  (Automatic sync layer)                                │    │
39
│  ├─────────────────────────────────────────────────────────┤    │
40
│  │ • Detects changes                                      │    │
41
│  │ • Queues for CloudKit                                  │    │
42
│  │ • Receives remote notifications                        │    │
43
│  │ • Applies merge policy                                 │    │
44
│  └──────────────┬──────────────────────┬────────────────┘    │
45
│                │                      │                      │
46
└────────────────┼──────────────────────┼──────────────────────┘
47
                │                      │
48
                ▼                      ▼
49
        ┌───────────────────┐  ┌──────────────────────┐
50
        │  Local SQLite DB  │  │  Persistent History  │
51
        │  (DeviceSettings) │  │  Journal             │
52
        └───────────────────┘  └──────────────────────┘
53
                │                      │
54
                └──────────┬───────────┘
55
                           │
56
                ┌──────────▼────────────┐
57
                │  CloudKit Framework   │
58
                │  (Device background   │
59
                │   process)            │
60
                └──────────┬────────────┘
61
                           │
62
                    [Network Sync]
63
                           │
64
                ┌──────────▼────────────┐
65
                │   iCloud Servers      │
66
                │   CloudKit Database   │
67
                └──────────┬────────────┘
68
                           │
69
                ┌──────────▼──────────────────┐
70
                │  Other Devices (iPhone,     │
71
                │  iPad, Mac) on same         │
72
                │  iCloud account             │
73
                └─────────────────────────────┘
74
```
75

            
76
---
77

            
78
## Local Write Flow (Sequence Diagram)
79

            
80
```
81
Device A              AppData              Core Data          CloudKit
82
  │                    │                      │                  │
83
  │─ User connects ───►│                      │                  │
84
  │ meter              │                      │                  │
85
  │                    │─ setConnection() ───►│                  │
86
  │                    │ (foreground QoS)     │                  │
87
  │                    │                      │                  │
88
  │◄─ UI Updates      │◄─ @Published change  │                  │
89
  │ (local)            │   (immediate)        │                  │
90
  │                    │                      │                  │
91
  │                    │─ context.save() ────►│                  │
92
  │                    │                      │                  │
93
  │                    │                      │─ Detect change   │
94
  │                    │                      │─ Create CKRecord │
95
  │                    │                      │─ Queue for sync ─►│
96
  │                    │                      │                  │─ Network upload
97
  │                    │                      │                  │ (1-2s)
98
  │                    │                      │                  │
99
  ┼────────────────────┼──────────────────────┼──────────────────┼───► [iCloud Server]
100
  │                    │                      │                  │
101
  │    (Device B receives remote change notification after ~5s)   │
102
```
103

            
104
---
105

            
106
## Remote Change Reception (Sequence Diagram)
107

            
108
```
109
Device B           System iOS            Core Data          AppData
110
  │                  │                      │                  │
111
  │               [iCloud notifies]         │                  │
112
  │                  │                      │                  │
113
  │                  │─ Remote change ─────►│                  │
114
  │                  │ notification posted  │                  │
115
  │                  │                      │                  │
116
  │                  │                      │─ Merge changes   │
117
  │                  │                      │ (merge policy)  │
118
  │                  │                      │─ Save locally   │
119
  │                  │                      │                  │
120
  │                  │─ NSPersistentStore ─►│─ remoteStoreDidChange()
121
  │                  │  RemoteChange        │                  │
122
  │                  │  notification        │                  │
123
  │                  │                      │                  │
124
  │                  │                      │◄─ reloadSettings()
125
  │                  │                      │ FromCloudStore()
126
  │◄─ UI updates ◄───────────────────────────────────────────────
127
  │ "Connected on                           │
128
  │  Device A"                              │
129
```
130

            
131
---
132

            
133
## Connection Lock Lifecycle
134

            
135
```
136
t=0s:  Device A connects
137
       ┌─────────────────────────────────────┐
138
       │ connectedByDeviceID = UUID_A        │
139
       │ connectedByDeviceName = "iPhone A"  │
140
       │ connectedAt = 2025-03-26 10:00:00   │
141
       │ connectedExpiryAt = 10:02:00        │ ◄─ TTL = 120s
142
       └─────────────────────────────────────┘
143
                │
144
                │ [CloudKit syncs]
145
                ▼
146
       [Device B sees connection]
147
       "Meter locked by iPhone A"
148

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

            
152
t=120s: Connection expires
153
       ┌─────────────────────────────────────┐
154
       │ connectedExpiryAt = 10:02:00         │
155
       │ now() > connectedExpiryAt ✓          │
156
       └─────────────────────────────────────┘
157
                │
158
       [Device B checks expiry]
159
                │
160
       if (now > connectedExpiryAt)
161
           Device B can connect!
162
```
163

            
164
---
165

            
166
## Merge Conflict Resolution
167

            
168
```
169
Device A                          Device B
170
t=0.5s: Sets meterName           t=0.6s: Sets meterName
171
  "Kitchen Meter"                  "Charger Meter"
172
  │                                │
173
  ▼                                ▼
174
  Save & Queue                     Save & Queue
175
  │                                │
176
  └───────────┬────────────────────┘
177
              ▼
178
        [Both pushed to CloudKit]
179
        (Race condition)
180

            
181
CloudKit Merge Policy:
182
  NSMergeByPropertyStoreTrumpMergePolicy
183
  → Latest write wins
184

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

            
190
---
191

            
192
## Rebuild Flow (v2 → v3)
193

            
194
```
195
User launches app (v3)
196
  │
197
  ▼
198
AppDelegate.didFinishLaunchingWithOptions
199
  │
200
  ▼
201
rebuildCanonicalStoreIfNeeded(version: 3)
202
  │
203
  ├─ Check UserDefaults["cloudStoreRebuildVersion.3"]
204
  │
205
  ├─ if already done:
206
  │   return (skip)
207
  │
208
  └─ if not done:
209
      │
210
      ├─ Fetch all DeviceSettings records
211
      │
212
      ├─ Group by normalized macAddress
213
      │
214
      ├─ For each group:
215
      │   ├─ Find "winner" (most data + most recent)
216
      │   └─ Merge others INTO winner
217
      │   └─ Delete duplicates
218
      │
219
      ├─ context.save()
220
      │   (CloudKit propagates deletes)
221
      │
222
      ├─ UserDefaults["cloudStoreRebuildVersion.3"] = true
223
      │
224
      └─ ✓ Done (won't run again)
225
```
226

            
227
---
228

            
229
## State Machine: Meter Connection
230

            
231
```
232
┌─────────────────┐
233
│     OFFLINE     │  ◄──────────────────────────────┐
234
└────────┬────────┘                                 │
235
         │                                          │
236
    advertisment heard
237
         │
238
         ▼
239
┌─────────────────────────────┐
240
│   PERIPHERAL_NOT_CONNECTED  │
241
└────────┬────────────────────┘
242
         │
243
    user clicks "Connect"
244
    Meter.connect() called
245
         │
246
         ▼
247
┌─────────────────────────────┐
248
│  PERIPHERAL_CONNECTION_PD   │
249
└────────┬────────────────────┘
250
         │
251
    BT services discovered
252
         │
253
         ▼
254
┌─────────────────────────────┐
255
│   PERIPHERAL_CONNECTED      │
256
│   (appData.publishMeterConn)│  ◄─────── CloudKit SyncStart
257
└────────┬────────────────────┘
258
         │
259
    peripheral ready for commands
260
         │
261
         ▼
262
┌─────────────────────────────┐
263
│   PERIPHERAL_READY          │
264
└────────┬────────────────────┘
265
         │
266
    data request sent
267
         │
268
         ▼
269
┌─────────────────────────────┐
270
│   COMMUNICATING             │
271
└────────┬────────────────────┘
272
         │
273
    data received
274
         │
275
         ▼
276
┌─────────────────────────────┐
277
│   DATA_IS_AVAILABLE         │ ──────── Cloud Polling Loop
278
└─────────────────────────────┘           (every 60s renew connection)
279
         │
280
    connect lost / timeout
281
         │
282
         ▼ (Or clear in UI)
283
    OFFLINE again
284
```
285

            
286
---
287

            
288
## Connection Renewal (Heartbeat)
289

            
290
```
291
┌─────────────────────────────┐
292
│ Meter.connect() called      │
293
│ operationalState changed    │
294
│ → .peripheralConnected      │
295
└──────────┬──────────────────┘
296
           │
297
           ▼
298
       startConnectionRenewal()
299
           │
300
           ├─ Create timer: interval = 60s
301
           │
302
           └─ repeats every 60s:
303
              │
304
              ├─ appData.publishMeterConnection()
305
              │  (updates connectedAt + connectedExpiryAt)
306
              │
307
              └─ CloudKit syncs (keeps lock alive)
308

            
309
Disconnection:
310
┌─────────────────────────────┐
311
│ operationalState changed    │
312
│ → .offline / .notConnected  │
313
└──────────┬──────────────────┘
314
           │
315
           ▼
316
       stopConnectionRenewal()
317
           │
318
           ├─ connectionRenewalTimer.invalidate()
319
           │
320
           └─ appData.clearMeterConnection()
321
              (removes connectedByDeviceID)
322
              │
323
              └─ CloudKit syncs (lock released)
324
```
325

            
326
---
327

            
328
## Error Recovery Paths
329

            
330
```
331
┌─────────────────────┐
332
│ Normal Operation    │
333
└─────────────────────┘
334
         │
335
         ▼ [Error: CloudKit unavailable]
336
┌──────────────────────────────┐
337
│ Queue changes locally         │
338
│ (stored in SQLite)           │
339
└──────────────────────────────┘
340
         │
341
         ▼ [Network restored]
342
┌──────────────────────────────┐
343
│ NSPersistentCloudKit         │
344
│ resumes sync automatically    │
345
└──────────────────────────────┘
346
         │
347
         ▼
348
┌─────────────────────┐
349
│ Back to normal      │
350
└─────────────────────┘
351

            
352
┌─────────────────────┐
353
│ Collision detected  │ (Device A & B both edited same field)
354
└─────────────────────┘
355
         │
356
         ▼
357
┌──────────────────────────────┐
358
│ NSMergeByPropertyStoreTrump   │
359
│ (CloudKit version wins)       │
360
└──────────────────────────────┘
361
         │
362
         └─ User sees local change revert ~1s after
363
              cloud update arrives (rare, acceptable)
364
```
365