@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,500 +1,411 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloudkit-sync
|
|
3
|
-
description: "
|
|
3
|
+
description: "CloudKit and iCloud sync on iOS and macOS: CKContainer public, private and shared databases, CKRecord, CKQuery, CKSubscription, CKSyncEngine, CKShare, NSUbiquitousKeyValueStore, iCloud Drive file coordination, SwiftData sync via ModelConfiguration cloudKitDatabase, CKError handling for conflicts, network failures and quota, iCloud account status checks. Use when implementing, reviewing or improving how an app stores or shares user data through iCloud."
|
|
4
4
|
metadata:
|
|
5
|
-
source:
|
|
5
|
+
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Enable iCloud + CloudKit in Signing & Capabilities. A container provides three databases:
|
|
33
|
-
|
|
34
|
-
| Database | Scope | Requires iCloud | Storage Quota |
|
|
35
|
-
|----------|-------|-----------------|---------------|
|
|
36
|
-
| Public | All users | Read: No, Write: Yes | App quota |
|
|
37
|
-
| Private | Current user | Yes | User quota |
|
|
38
|
-
| Shared | Shared records | Yes | Owner quota |
|
|
7
|
+
|
|
8
|
+
# CloudKit Sync
|
|
9
|
+
|
|
10
|
+
Everything iCloud offers for app data: CloudKit databases, the key-value
|
|
11
|
+
store, and iCloud Drive documents. This file covers containers, record
|
|
12
|
+
operations, queries, subscriptions, `CKSyncEngine`, SwiftData integration,
|
|
13
|
+
error handling and conflict merges for Swift 6.3 on iOS 26, with older
|
|
14
|
+
availability noted where it matters.
|
|
15
|
+
|
|
16
|
+
Incremental fetch operations, change tokens, sharing, zones, assets, batching,
|
|
17
|
+
quality of service, encrypted fields and the Dashboard are in the
|
|
18
|
+
[CloudKit patterns reference](references/cloudkit-patterns.md). Core Data
|
|
19
|
+
stacks that sync through `NSPersistentCloudKitContainer` start in `core-data`;
|
|
20
|
+
background push handling in general is `push-notifications`.
|
|
21
|
+
|
|
22
|
+
## Containers and databases
|
|
23
|
+
|
|
24
|
+
Enable **iCloud** with the **CloudKit** service under Signing & Capabilities.
|
|
25
|
+
|
|
26
|
+
| Database | Who sees it | Needs an iCloud account | Storage charged to |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| Public | Every user of the app | Only for writes; reads work signed out | The app |
|
|
29
|
+
| Private | The signed-in user | Yes | The user |
|
|
30
|
+
| Shared | Records others shared with the user | Yes | The owner |
|
|
39
31
|
|
|
40
32
|
```swift
|
|
41
33
|
import CloudKit
|
|
42
34
|
|
|
43
|
-
let container = CKContainer.default()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let publicDB = container.publicCloudDatabase
|
|
35
|
+
let container = CKContainer(identifier: "iCloud.app.gardenlog") // or CKContainer.default()
|
|
36
|
+
let publicDB = container.publicCloudDatabase
|
|
47
37
|
let privateDB = container.privateCloudDatabase
|
|
48
|
-
let sharedDB
|
|
38
|
+
let sharedDB = container.sharedCloudDatabase
|
|
49
39
|
```
|
|
50
40
|
|
|
51
|
-
##
|
|
41
|
+
## Records
|
|
52
42
|
|
|
53
|
-
|
|
43
|
+
A `CKRecord` is a typed dictionary. One record can hold up to 1 MB, not
|
|
44
|
+
counting the files behind any `CKAsset` fields.
|
|
54
45
|
|
|
55
46
|
```swift
|
|
56
|
-
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
47
|
+
func plant(named name: String, in db: CKDatabase) async throws -> CKRecord {
|
|
48
|
+
let plant = CKRecord(recordType: "Plant")
|
|
49
|
+
plant["name"] = name as CKRecordValue
|
|
50
|
+
plant["plantedOn"] = Date.now as CKRecordValue
|
|
51
|
+
plant["tags"] = ["herb", "sunny"] as CKRecordValue
|
|
52
|
+
return try await db.save(plant)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func rename(_ recordName: String, to newName: String, in db: CKDatabase) async throws {
|
|
56
|
+
let id = CKRecord.ID(recordName: recordName)
|
|
57
|
+
let existing = try await db.record(for: id) // always fetch before editing
|
|
58
|
+
existing["name"] = newName as CKRecordValue
|
|
59
|
+
_ = try await db.save(existing)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func uproot(_ recordName: String, in db: CKDatabase) async throws {
|
|
63
|
+
_ = try await db.deleteRecord(withID: CKRecord.ID(recordName: recordName))
|
|
64
|
+
}
|
|
74
65
|
```
|
|
75
66
|
|
|
76
|
-
### Custom
|
|
67
|
+
### Custom zones
|
|
77
68
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
commits, change tracking
|
|
69
|
+
The private database accepts zones the app creates; the shared database
|
|
70
|
+
exposes the zones other people shared with the user. Custom zones unlock
|
|
71
|
+
atomic commits, change tracking and sharing. The public database has no custom
|
|
81
72
|
zones.
|
|
82
73
|
|
|
83
74
|
```swift
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
try await privateDB.save(zone)
|
|
75
|
+
let beds = CKRecordZone.ID(zoneName: "GardenBeds")
|
|
76
|
+
_ = try await privateDB.save(CKRecordZone(zoneID: beds))
|
|
87
77
|
|
|
88
|
-
let
|
|
89
|
-
let
|
|
78
|
+
let bedID = CKRecord.ID(recordName: UUID().uuidString, zoneID: beds)
|
|
79
|
+
let bed = CKRecord(recordType: "Bed", recordID: bedID)
|
|
90
80
|
```
|
|
91
81
|
|
|
92
|
-
##
|
|
93
|
-
|
|
94
|
-
Query records with NSPredicate. Supported: `==`, `!=`, `<`, `>`, `<=`, `>=`,
|
|
95
|
-
`BEGINSWITH`, `CONTAINS`, `IN`, `AND`, `NOT`, `BETWEEN`,
|
|
96
|
-
`distanceToLocation:fromLocation:`.
|
|
82
|
+
## Queries
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
`self CONTAINS`. `BEGINSWITH` is the string-prefix operator; unsupported
|
|
100
|
-
operators, key paths, or field types fail when the query executes.
|
|
101
|
-
For every encryption review, explicitly call out field eligibility: encrypted
|
|
102
|
-
values cannot be queried or sorted; `CKAsset` is encrypted by default; and
|
|
103
|
-
`CKRecord.Reference` cannot be encrypted because CloudKit needs it server-side.
|
|
84
|
+
CloudKit accepts a subset of `NSPredicate`:
|
|
104
85
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let (results, _) = try await privateDB.records(matching: query)
|
|
111
|
-
for (_, result) in results {
|
|
112
|
-
let record = try result.get()
|
|
113
|
-
print(record["title"] as? String ?? "")
|
|
114
|
-
}
|
|
86
|
+
- comparisons `==`, `!=`, `<`, `>`, `<=`, `>=`
|
|
87
|
+
- `BEGINSWITH` for string prefixes
|
|
88
|
+
- `CONTAINS` for membership in a list field, except `self CONTAINS`, which runs a tokenized full-text search
|
|
89
|
+
- `IN`, `AND`, `NOT`, `BETWEEN`
|
|
90
|
+
- `distanceToLocation:fromLocation:` for location fields
|
|
115
91
|
|
|
116
|
-
|
|
117
|
-
|
|
92
|
+
An unsupported operator, key path or field type is only rejected when the
|
|
93
|
+
query executes, not when you build it.
|
|
118
94
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
predicate: NSPredicate(format: "self CONTAINS %@", "roadmap")
|
|
123
|
-
)
|
|
95
|
+
Encrypted values cannot be queried or sorted. `CKAsset` contents are already
|
|
96
|
+
encrypted, and a `CKRecord.Reference` can never be encrypted because the
|
|
97
|
+
server needs it. Say all three whenever you review encryption.
|
|
124
98
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
99
|
+
```swift
|
|
100
|
+
func recentHerbs(in db: CKDatabase) async throws -> [CKRecord] {
|
|
101
|
+
let weekAgo = Date.now.addingTimeInterval(-7 * 86_400)
|
|
102
|
+
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
|
103
|
+
NSPredicate(format: "plantedOn > %@", weekAgo as NSDate),
|
|
104
|
+
NSPredicate(format: "tags CONTAINS %@", "herb"),
|
|
105
|
+
])
|
|
106
|
+
let query = CKQuery(recordType: "Plant", predicate: predicate)
|
|
107
|
+
query.sortDescriptors = [NSSortDescriptor(key: "plantedOn", ascending: false)]
|
|
108
|
+
|
|
109
|
+
let (matches, _) = try await db.records(matching: query)
|
|
110
|
+
return matches.compactMap { try? $0.1.get() }
|
|
111
|
+
}
|
|
130
112
|
```
|
|
131
113
|
|
|
132
|
-
|
|
114
|
+
Use `NSPredicate(value: true)` to match every record of a type.
|
|
133
115
|
|
|
134
|
-
Subscriptions
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
116
|
+
## Subscriptions
|
|
117
|
+
|
|
118
|
+
A subscription makes CloudKit push a notification when server data changes.
|
|
119
|
+
With CloudKit enabled, Xcode manages the push entitlement; no separate App ID
|
|
120
|
+
push setup is needed. Silent pushes still require **Background Modes >
|
|
121
|
+
Remote notifications**.
|
|
138
122
|
|
|
139
123
|
```swift
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
subscriptionID: "urgent-notes",
|
|
124
|
+
let newPlants = CKQuerySubscription(
|
|
125
|
+
recordType: "Plant",
|
|
126
|
+
predicate: NSPredicate(value: true),
|
|
127
|
+
subscriptionID: "plant-changes",
|
|
145
128
|
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
|
|
146
129
|
)
|
|
147
|
-
let
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
try await
|
|
151
|
-
|
|
152
|
-
// Database subscription -- fires on any database change
|
|
153
|
-
let dbSub = CKDatabaseSubscription(subscriptionID: "private-db-changes")
|
|
154
|
-
dbSub.notificationInfo = notifInfo
|
|
155
|
-
try await privateDB.save(dbSub)
|
|
156
|
-
|
|
157
|
-
// Record zone subscription -- fires on changes within a zone
|
|
158
|
-
let zoneSub = CKRecordZoneSubscription(
|
|
159
|
-
zoneID: CKRecordZone.ID(zoneName: "NotesZone"),
|
|
160
|
-
subscriptionID: "notes-zone-changes"
|
|
161
|
-
)
|
|
162
|
-
zoneSub.notificationInfo = notifInfo
|
|
163
|
-
try await privateDB.save(zoneSub)
|
|
164
|
-
```
|
|
130
|
+
let info = CKSubscription.NotificationInfo()
|
|
131
|
+
info.shouldSendContentAvailable = true // silent
|
|
132
|
+
newPlants.notificationInfo = info
|
|
133
|
+
_ = try await publicDB.save(newPlants)
|
|
165
134
|
|
|
166
|
-
|
|
135
|
+
let everything = CKDatabaseSubscription(subscriptionID: "private-db") // any change in the DB
|
|
136
|
+
let oneZone = CKRecordZoneSubscription(zoneID: beds, subscriptionID: "beds") // changes in one zone
|
|
137
|
+
```
|
|
167
138
|
|
|
168
139
|
```swift
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
guard
|
|
175
|
-
|
|
176
|
-
|
|
140
|
+
import UIKit
|
|
141
|
+
|
|
142
|
+
// UIApplicationDelegate
|
|
143
|
+
func application(_ application: UIApplication,
|
|
144
|
+
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
|
145
|
+
guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo),
|
|
146
|
+
note.subscriptionID == "private-db" else { return .noData }
|
|
147
|
+
do {
|
|
148
|
+
try await GardenSync.shared.pullNow() // CKSyncEngine or CKFetchRecordZoneChangesOperation
|
|
149
|
+
return .newData
|
|
150
|
+
} catch {
|
|
151
|
+
return .failed
|
|
152
|
+
}
|
|
177
153
|
}
|
|
178
154
|
```
|
|
179
155
|
|
|
180
156
|
## CKSyncEngine (iOS 17+)
|
|
181
157
|
|
|
182
|
-
`CKSyncEngine` is the recommended
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
158
|
+
For custom model data, `CKSyncEngine` is the recommended route. It schedules
|
|
159
|
+
work, retries transient failures, keeps change tokens and manages the
|
|
160
|
+
database subscription for you. It does **not** resolve app-level save
|
|
161
|
+
failures: a `CKError.serverRecordChanged` inside
|
|
162
|
+
`sentRecordZoneChanges.failedRecordSaves` needs your merge and a reschedule.
|
|
163
|
+
Its automatic timing is not predictable. It needs the CloudKit capability plus
|
|
164
|
+
Remote notifications, and works only with private and shared databases, never
|
|
165
|
+
public.
|
|
188
166
|
|
|
189
167
|
```swift
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let syncEngine: CKSyncEngine
|
|
168
|
+
actor GardenSync: CKSyncEngineDelegate {
|
|
169
|
+
static let shared = GardenSync()
|
|
170
|
+
private var engine: CKSyncEngine?
|
|
194
171
|
|
|
195
|
-
|
|
172
|
+
func start() {
|
|
173
|
+
guard engine == nil else { return }
|
|
196
174
|
let config = CKSyncEngine.Configuration(
|
|
197
|
-
database:
|
|
198
|
-
stateSerialization:
|
|
175
|
+
database: CKContainer.default().privateCloudDatabase,
|
|
176
|
+
stateSerialization: StateFile.load(), // CKSyncEngine.State.Serialization?
|
|
199
177
|
delegate: self
|
|
200
178
|
)
|
|
201
|
-
|
|
179
|
+
engine = CKSyncEngine(config)
|
|
202
180
|
}
|
|
203
181
|
|
|
204
182
|
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
|
|
205
183
|
switch event {
|
|
206
184
|
case .stateUpdate(let update):
|
|
207
|
-
|
|
185
|
+
StateFile.save(update.stateSerialization) // resume from the right token next launch
|
|
208
186
|
case .accountChange(let change):
|
|
209
|
-
|
|
210
|
-
case .fetchedRecordZoneChanges(let
|
|
211
|
-
for mod in
|
|
212
|
-
for
|
|
187
|
+
await LocalStore.shared.handle(change)
|
|
188
|
+
case .fetchedRecordZoneChanges(let fetched):
|
|
189
|
+
for mod in fetched.modifications { await LocalStore.shared.upsert(mod.record) }
|
|
190
|
+
for gone in fetched.deletions { await LocalStore.shared.remove(gone.recordID) }
|
|
213
191
|
case .sentRecordZoneChanges(let sent):
|
|
214
|
-
for saved in sent.savedRecords { markSynced(saved) }
|
|
215
|
-
for
|
|
216
|
-
default:
|
|
192
|
+
for saved in sent.savedRecords { await LocalStore.shared.markSynced(saved) }
|
|
193
|
+
for failure in sent.failedRecordSaves { await resolve(failure, engine: syncEngine) }
|
|
194
|
+
default:
|
|
195
|
+
break
|
|
217
196
|
}
|
|
218
197
|
}
|
|
219
198
|
|
|
220
|
-
func nextRecordZoneChangeBatch(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
pendingChanges: pending
|
|
228
|
-
) { recordID in self.recordToSend(for: recordID) }
|
|
199
|
+
func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
|
|
200
|
+
syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
|
|
201
|
+
let scope = context.options.scope
|
|
202
|
+
let pending = syncEngine.state.pendingRecordZoneChanges.filter { scope.contains($0) }
|
|
203
|
+
return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: pending) { id in
|
|
204
|
+
await LocalStore.shared.record(for: id)
|
|
205
|
+
}
|
|
229
206
|
}
|
|
230
|
-
}
|
|
231
207
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
syncEngine.state.add(pendingRecordZoneChanges: [.saveRecord(recordID)])
|
|
208
|
+
func queueUpload(of id: CKRecord.ID) {
|
|
209
|
+
engine?.state.add(pendingRecordZoneChanges: [.saveRecord(id)])
|
|
210
|
+
}
|
|
236
211
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
try await
|
|
212
|
+
func pullNow() async throws {
|
|
213
|
+
guard let engine else { return }
|
|
214
|
+
try await engine.fetchChanges() // pull-to-refresh or push arrival
|
|
215
|
+
try await engine.sendChanges()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
240
218
|
```
|
|
241
219
|
|
|
242
|
-
|
|
243
|
-
|
|
220
|
+
`context.options.scope` says which zones (for example `.zoneIDs([...])`) or
|
|
221
|
+
records this send covers; filter the pending changes to it rather than
|
|
222
|
+
sending everything. Both delegate methods are `async`. Persist
|
|
223
|
+
`stateSerialization` on every `.stateUpdate`; without it the engine starts
|
|
224
|
+
over on the next launch.
|
|
244
225
|
|
|
245
|
-
## SwiftData
|
|
226
|
+
## SwiftData with CloudKit
|
|
246
227
|
|
|
247
|
-
`ModelConfiguration`
|
|
248
|
-
implementation or review
|
|
228
|
+
`ModelConfiguration` can sync a SwiftData store to CloudKit. Any
|
|
229
|
+
implementation or review has to state **two verdicts**:
|
|
249
230
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
231
|
+
1. **Model compatibility** - no `#Unique` macro or unique attributes, every
|
|
232
|
+
relationship optional, no `.deny` delete rule, and large `Data` marked for
|
|
233
|
+
external storage. Scalar properties do not all need to be optional; they
|
|
234
|
+
just need defaults or optionality where CloudKit requires it.
|
|
235
|
+
2. **Schema rollout** - initialize the development schema from a non-release
|
|
236
|
+
build, inspect it in CloudKit Dashboard, and promote it to production before
|
|
237
|
+
shipping. After promotion, changes are additive only: never delete a model
|
|
238
|
+
type or change an existing attribute.
|
|
256
239
|
|
|
257
240
|
```swift
|
|
258
241
|
import SwiftData
|
|
259
242
|
|
|
260
243
|
@Model
|
|
261
|
-
class
|
|
262
|
-
var
|
|
263
|
-
var
|
|
264
|
-
var
|
|
265
|
-
@Attribute(.externalStorage) var
|
|
266
|
-
|
|
267
|
-
init(
|
|
268
|
-
self.title = title
|
|
269
|
-
self.body = body
|
|
270
|
-
self.createdAt = Date()
|
|
271
|
-
}
|
|
244
|
+
final class Harvest {
|
|
245
|
+
var crop: String = ""
|
|
246
|
+
var weightGrams: Double = 0
|
|
247
|
+
var pickedOn: Date = Date.now
|
|
248
|
+
@Attribute(.externalStorage) var photo: Data?
|
|
249
|
+
var bed: GardenBed? // optional relationship
|
|
250
|
+
init() {}
|
|
272
251
|
}
|
|
252
|
+
```
|
|
273
253
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
let container = try ModelContainer(for: Note.self, configurations: config)
|
|
254
|
+
```swift
|
|
255
|
+
let config = ModelConfiguration("Garden", cloudKitDatabase: .private("iCloud.app.gardenlog"))
|
|
256
|
+
let container = try ModelContainer(for: Harvest.self, GardenBed.self, configurations: config)
|
|
279
257
|
```
|
|
280
258
|
|
|
281
|
-
##
|
|
259
|
+
## Key-value store
|
|
282
260
|
|
|
283
|
-
|
|
284
|
-
locally when iCloud is unavailable
|
|
261
|
+
`NSUbiquitousKeyValueStore` holds small preferences: at most 1024 keys, 1 MB
|
|
262
|
+
in total and 1 MB per value. It writes locally when iCloud is unavailable and
|
|
263
|
+
syncs later.
|
|
285
264
|
|
|
286
265
|
```swift
|
|
287
|
-
let
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
kvStore.set(14.0, forKey: "fontSize")
|
|
292
|
-
kvStore.set(true, forKey: "notificationsEnabled")
|
|
293
|
-
kvStore.synchronize()
|
|
266
|
+
let kv = NSUbiquitousKeyValueStore.default
|
|
267
|
+
kv.set("celsius", forKey: "temperatureUnit")
|
|
268
|
+
kv.synchronize()
|
|
269
|
+
let unit = kv.string(forKey: "temperatureUnit")
|
|
294
270
|
|
|
295
|
-
// Read
|
|
296
|
-
let theme = kvStore.string(forKey: "theme") ?? "system"
|
|
297
|
-
|
|
298
|
-
// Observe external changes
|
|
299
271
|
NotificationCenter.default.addObserver(
|
|
300
272
|
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
|
301
|
-
object:
|
|
302
|
-
) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
let keys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String]
|
|
306
|
-
else { return }
|
|
307
|
-
|
|
273
|
+
object: kv, queue: .main
|
|
274
|
+
) { note in
|
|
275
|
+
let reason = note.userInfo?[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int
|
|
276
|
+
let keys = note.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] ?? []
|
|
308
277
|
switch reason {
|
|
309
|
-
case NSUbiquitousKeyValueStoreServerChange:
|
|
310
|
-
|
|
311
|
-
case
|
|
312
|
-
reloadAllSettings()
|
|
313
|
-
case NSUbiquitousKeyValueStoreQuotaViolationChange:
|
|
314
|
-
handleQuotaExceeded()
|
|
278
|
+
case NSUbiquitousKeyValueStoreServerChange: Preferences.apply(keys)
|
|
279
|
+
case NSUbiquitousKeyValueStoreInitialSyncChange: Preferences.reloadAll()
|
|
280
|
+
case NSUbiquitousKeyValueStoreQuotaViolationChange: Preferences.trimStorage()
|
|
315
281
|
default: break
|
|
316
282
|
}
|
|
317
283
|
}
|
|
318
284
|
```
|
|
319
285
|
|
|
320
|
-
## iCloud Drive
|
|
286
|
+
## iCloud Drive documents
|
|
321
287
|
|
|
322
|
-
|
|
323
|
-
`url(forUbiquityContainerIdentifier:)` and
|
|
324
|
-
`setUbiquitous`
|
|
325
|
-
|
|
288
|
+
Documents sync through the `FileManager` ubiquity APIs. Both
|
|
289
|
+
`url(forUbiquityContainerIdentifier:)` and
|
|
290
|
+
`setUbiquitous(_:itemAt:destinationURL:)` can block (the second performs
|
|
291
|
+
coordinated file operations), so call them off the main thread. A `nil`
|
|
292
|
+
container URL means iCloud is not available. If the app is currently
|
|
293
|
+
presenting the file, register a file presenter before moving it.
|
|
326
294
|
|
|
327
295
|
```swift
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
try FileManager.default.setUbiquitous(true, itemAt: localURL, destinationURL: cloudURL)
|
|
296
|
+
func moveToCloud(_ local: URL) {
|
|
297
|
+
Task.detached {
|
|
298
|
+
let fm = FileManager.default
|
|
299
|
+
guard let root = fm.url(forUbiquityContainerIdentifier: nil) else { return }
|
|
300
|
+
let docs = root.appending(path: "Documents", directoryHint: .isDirectory)
|
|
301
|
+
try? fm.createDirectory(at: docs, withIntermediateDirectories: true)
|
|
302
|
+
try? fm.setUbiquitous(true, itemAt: local, destinationURL: docs.appending(path: local.lastPathComponent))
|
|
303
|
+
}
|
|
337
304
|
}
|
|
338
305
|
```
|
|
339
306
|
|
|
340
|
-
|
|
341
|
-
`NSMetadataQueryUbiquitousDocumentsScope` or
|
|
342
|
-
`NSMetadataQueryUbiquitousDataScope`.
|
|
307
|
+
Watch for changes with an `NSMetadataQuery` whose search scope is
|
|
308
|
+
`NSMetadataQueryUbiquitousDocumentsScope` or `NSMetadataQueryUbiquitousDataScope`.
|
|
343
309
|
|
|
344
|
-
## Account
|
|
310
|
+
## Account status
|
|
345
311
|
|
|
346
|
-
|
|
312
|
+
Check the account before any sync, and listen for `.CKAccountChanged`.
|
|
347
313
|
|
|
348
314
|
```swift
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
case .couldNotDetermine: throw SyncError.unknown
|
|
357
|
-
@unknown default: throw SyncError.unknown
|
|
358
|
-
}
|
|
315
|
+
switch try await CKContainer.default().accountStatus() {
|
|
316
|
+
case .available: await GardenSync.shared.start()
|
|
317
|
+
case .noAccount: showSignInToICloudHint()
|
|
318
|
+
case .restricted: showRestrictedNotice()
|
|
319
|
+
case .temporarilyUnavailable: scheduleAccountRecheck()
|
|
320
|
+
case .couldNotDetermine: scheduleAccountRecheck()
|
|
321
|
+
@unknown default: scheduleAccountRecheck()
|
|
359
322
|
}
|
|
360
323
|
```
|
|
361
324
|
|
|
362
|
-
|
|
325
|
+
## CKError handling
|
|
363
326
|
|
|
364
|
-
|
|
|
365
|
-
|
|
366
|
-
| `.networkFailure`, `.networkUnavailable` | Queue
|
|
367
|
-
| `.serverRecordChanged` | Three-way merge (
|
|
368
|
-
| `.requestRateLimited`, `.zoneBusy`, `.serviceUnavailable` |
|
|
369
|
-
| `.quotaExceeded` |
|
|
370
|
-
| `.notAuthenticated` |
|
|
371
|
-
| `.partialFailure` |
|
|
372
|
-
| `.changeTokenExpired` |
|
|
373
|
-
| `.userDeletedZone` | Recreate zone and
|
|
327
|
+
| Code | Response |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `.networkFailure`, `.networkUnavailable` | Queue the work and retry once connectivity returns |
|
|
330
|
+
| `.serverRecordChanged` | Three-way merge (below) |
|
|
331
|
+
| `.requestRateLimited`, `.zoneBusy`, `.serviceUnavailable` | Wait `retryAfterSeconds` before retrying |
|
|
332
|
+
| `.quotaExceeded` | Tell the user; reduce stored data |
|
|
333
|
+
| `.notAuthenticated` | Ask the user to sign in to iCloud |
|
|
334
|
+
| `.partialFailure` | Walk `partialErrorsByItemID` and handle each item's error |
|
|
335
|
+
| `.changeTokenExpired` | Drop the token and refetch everything |
|
|
336
|
+
| `.userDeletedZone` | Recreate the zone and upload again |
|
|
374
337
|
|
|
375
338
|
```swift
|
|
376
|
-
func
|
|
377
|
-
|
|
378
|
-
switch ckError.code {
|
|
379
|
-
case .networkFailure, .networkUnavailable:
|
|
380
|
-
scheduleRetryWhenOnline()
|
|
381
|
-
case .serverRecordChanged:
|
|
382
|
-
resolveConflict(ckError)
|
|
383
|
-
case .requestRateLimited, .zoneBusy, .serviceUnavailable:
|
|
384
|
-
let delay = ckError.retryAfterSeconds ?? 3.0
|
|
385
|
-
scheduleRetry(after: delay)
|
|
386
|
-
case .quotaExceeded:
|
|
387
|
-
notifyUserStorageFull()
|
|
388
|
-
case .partialFailure:
|
|
389
|
-
if let partial = ckError.partialErrorsByItemID {
|
|
390
|
-
for (_, itemError) in partial { handleCloudKitError(itemError) }
|
|
391
|
-
}
|
|
392
|
-
case .changeTokenExpired:
|
|
393
|
-
resetChangeToken()
|
|
394
|
-
case .userDeletedZone:
|
|
395
|
-
recreateZoneAndResync()
|
|
396
|
-
default: logError(ckError)
|
|
397
|
-
}
|
|
339
|
+
func retryDelay(for error: CKError) -> TimeInterval {
|
|
340
|
+
error.retryAfterSeconds ?? 3
|
|
398
341
|
}
|
|
399
342
|
```
|
|
400
343
|
|
|
401
|
-
## Conflict
|
|
344
|
+
## Conflict resolution
|
|
402
345
|
|
|
403
|
-
|
|
404
|
-
`.
|
|
405
|
-
|
|
346
|
+
A `.serverRecordChanged` error carries `ancestorRecord`, `clientRecord` and
|
|
347
|
+
`serverRecord`. Always merge **into `serverRecord`**, because it holds the
|
|
348
|
+
current change tag; saving the client copy just fails again.
|
|
406
349
|
|
|
407
350
|
```swift
|
|
408
|
-
func
|
|
409
|
-
guard error.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if
|
|
418
|
-
|
|
419
|
-
} else if client[key] == ancestor[key] {
|
|
420
|
-
// Client unchanged, keep server (already there)
|
|
351
|
+
func merged(_ error: CKError) -> CKRecord? {
|
|
352
|
+
guard let server = error.serverRecord, let mine = error.clientRecord else { return nil }
|
|
353
|
+
let base = error.ancestorRecord
|
|
354
|
+
for key in mine.changedKeys() {
|
|
355
|
+
let serverValue = server[key] as? NSObject
|
|
356
|
+
let baseValue = base?[key] as? NSObject
|
|
357
|
+
let myValue = mine[key] as? NSObject
|
|
358
|
+
if serverValue == baseValue {
|
|
359
|
+
server[key] = mine[key] // only I changed it
|
|
360
|
+
} else if myValue == baseValue {
|
|
361
|
+
continue // only the server changed it
|
|
421
362
|
} else {
|
|
422
|
-
server[key] =
|
|
423
|
-
ancestor: ancestor[key], client: client[key], server: server[key])
|
|
363
|
+
server[key] = resolveBothChanged(key, mine: mine[key], theirs: server[key])
|
|
424
364
|
}
|
|
425
365
|
}
|
|
426
|
-
|
|
427
|
-
Task { try await CKContainer.default().privateCloudDatabase.save(server) }
|
|
366
|
+
return server // save this one
|
|
428
367
|
}
|
|
429
368
|
```
|
|
430
369
|
|
|
431
|
-
## Common
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
try await privateDB.save(sub)
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
**DON'T:** Retry immediately on rate limiting.
|
|
463
|
-
**DO:** Use `CKError.retryAfterSeconds` to wait the required duration.
|
|
464
|
-
|
|
465
|
-
**DON'T:** Assume `CKSyncEngine` handles `.serverRecordChanged` conflicts for you.
|
|
466
|
-
**DO:** Resolve `failedRecordSaves` with a three-way merge, then reschedule the save.
|
|
467
|
-
|
|
468
|
-
**DON'T:** Pass nil change token on every fetch.
|
|
469
|
-
**DO:** Persist change tokens to disk and supply them on subsequent fetches.
|
|
470
|
-
|
|
471
|
-
## Review Checklist
|
|
472
|
-
|
|
473
|
-
- [ ] iCloud + CloudKit capability enabled in Signing & Capabilities
|
|
474
|
-
- [ ] Account status checked before sync; `.noAccount` handled gracefully
|
|
475
|
-
- [ ] Private database used for user data; public only for shared content
|
|
476
|
-
- [ ] Custom record zones created in private DB; shared DB zones discovered from shares
|
|
477
|
-
- [ ] `CKError.serverRecordChanged` handled with three-way merge into `serverRecord`
|
|
478
|
-
- [ ] Network failures queued for retry; `retryAfterSeconds` respected
|
|
479
|
-
- [ ] `CKDatabaseSubscription` or `CKSyncEngine` used for push-based sync; Remote notifications enabled for background delivery
|
|
480
|
-
- [ ] Change tokens persisted to disk; `changeTokenExpired` resets and refetches
|
|
481
|
-
- [ ] `.partialFailure` errors inspected per-item via `partialErrorsByItemID`
|
|
482
|
-
- [ ] `.userDeletedZone` handled by recreating zone and resyncing
|
|
483
|
-
- [ ] SwiftData CloudKit review reports model compatibility and schema rollout: initialized/verified development schema, promoted before release, and additive-only production changes
|
|
484
|
-
- [ ] `NSUbiquitousKeyValueStore.didChangeExternallyNotification` observed
|
|
485
|
-
- [ ] Encryption review says `CKRecord.Reference` cannot use `encryptedValues` because CloudKit needs it server-side; no query/sort on encrypted fields; `CKAsset` is encrypted by default
|
|
486
|
-
- [ ] `CKSyncEngine` state serialization persisted across launches (iOS 17+)
|
|
370
|
+
## Common mistakes
|
|
371
|
+
|
|
372
|
+
- Starting sync without confirming `accountStatus() == .available`.
|
|
373
|
+
- Ignoring `.serverRecordChanged`, which silently loses edits.
|
|
374
|
+
- Putting personal data in the public database. Private is for user data;
|
|
375
|
+
public is only for content every user should see.
|
|
376
|
+
- Polling on a timer. Use a `CKDatabaseSubscription` with silent pushes or `CKSyncEngine`.
|
|
377
|
+
- Retrying a rate-limited request immediately instead of waiting `retryAfterSeconds`.
|
|
378
|
+
- Expecting `CKSyncEngine` to settle `.serverRecordChanged`. Merge the
|
|
379
|
+
`failedRecordSaves` yourself and reschedule them.
|
|
380
|
+
- Fetching with a `nil` change token every time. Store tokens on disk.
|
|
381
|
+
|
|
382
|
+
## Review checklist
|
|
383
|
+
|
|
384
|
+
- [ ] iCloud capability with CloudKit enabled
|
|
385
|
+
- [ ] Account status checked; `.noAccount` handled
|
|
386
|
+
- [ ] User data kept in the private database
|
|
387
|
+
- [ ] Custom zones in the private database; shared zones discovered from accepted shares
|
|
388
|
+
- [ ] `.serverRecordChanged` merged into `serverRecord`
|
|
389
|
+
- [ ] Network failures queued; `retryAfterSeconds` honored
|
|
390
|
+
- [ ] Sync driven by pushes; Remote notifications background mode on
|
|
391
|
+
- [ ] Change tokens persisted; `.changeTokenExpired` resets them
|
|
392
|
+
- [ ] `.partialFailure` examined item by item
|
|
393
|
+
- [ ] `.userDeletedZone` handled
|
|
394
|
+
- [ ] SwiftData review gives both the model-compatibility and schema-rollout verdicts
|
|
395
|
+
- [ ] Key-value store external-change notification observed
|
|
396
|
+
- [ ] Encryption review mentions: references cannot be encrypted, encrypted fields cannot be queried or sorted, assets are encrypted already
|
|
397
|
+
- [ ] `CKSyncEngine` state serialization saved (iOS 17+)
|
|
487
398
|
|
|
488
399
|
## References
|
|
489
400
|
|
|
490
|
-
-
|
|
491
|
-
- [CloudKit
|
|
492
|
-
- [CKContainer](https://
|
|
493
|
-
- [CKRecord](https://
|
|
494
|
-
- [CKQuery](https://
|
|
495
|
-
- [CKSubscription](https://
|
|
496
|
-
- [CKSyncEngine](https://
|
|
497
|
-
- [CKShare](https://
|
|
498
|
-
- [CKError](https://
|
|
499
|
-
- [NSUbiquitousKeyValueStore](https://
|
|
500
|
-
-
|
|
401
|
+
- [CloudKit patterns](references/cloudkit-patterns.md) - incremental fetches, tokens, sharing, zones, assets, batches, QoS, encryption, Dashboard
|
|
402
|
+
- [CloudKit](https://developer.apple.com/documentation/cloudkit)
|
|
403
|
+
- [CKContainer](https://developer.apple.com/documentation/cloudkit/ckcontainer)
|
|
404
|
+
- [CKRecord](https://developer.apple.com/documentation/cloudkit/ckrecord)
|
|
405
|
+
- [CKQuery](https://developer.apple.com/documentation/cloudkit/ckquery)
|
|
406
|
+
- [CKSubscription](https://developer.apple.com/documentation/cloudkit/cksubscription)
|
|
407
|
+
- [CKSyncEngine](https://developer.apple.com/documentation/cloudkit/cksyncengine-5sie5)
|
|
408
|
+
- [CKShare](https://developer.apple.com/documentation/cloudkit/ckshare)
|
|
409
|
+
- [CKError](https://developer.apple.com/documentation/cloudkit/ckerror)
|
|
410
|
+
- [NSUbiquitousKeyValueStore](https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore)
|
|
411
|
+
- SwiftData: the article on syncing model data across a person's devices
|