@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,498 +1,422 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: core-data
|
|
3
|
-
description: "
|
|
3
|
+
description: "Create, audit or refine Core Data persistence for apps still on Core Data rather than SwiftData: stack setup, NSManagedObject subclasses, NSFetchedResultsController for list UIs, NSBatchInsertRequest, NSBatchDeleteRequest and NSBatchUpdateRequest bulk work, NSPersistentHistoryChangeRequest history tracking across app and extension targets, NSStagedMigrationManager staged migrations and NSCompositeAttributeDescription composite attributes (iOS 17+), and making Core Data contexts work with Swift Concurrency. Use when working on an existing Core Data store. Not for Core Data and SwiftData coexistence or migration; use the swiftdata skill."
|
|
4
4
|
metadata:
|
|
5
|
-
source:
|
|
5
|
+
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Core Data
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
and
|
|
10
|
+
For apps that still persist with Core Data rather than SwiftData. Covers the
|
|
11
|
+
stack, queue rules and Swift Concurrency, fetched results controllers, batch
|
|
12
|
+
requests, persistent history, staged migration, composite attributes and
|
|
13
|
+
testing. Hand-offs: SwiftData coexistence and migration go to `swiftdata`;
|
|
14
|
+
iCloud sync details (zones, sharing, `CKError`) go to `cloudkit-sync`; test
|
|
15
|
+
style goes to `swift-testing`.
|
|
14
16
|
|
|
15
|
-
##
|
|
17
|
+
## The stack
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [NSFetchedResultsController](#nsfetchedresultscontroller)
|
|
20
|
-
- [Batch Operations](#batch-operations)
|
|
21
|
-
- [Persistent History Tracking](#persistent-history-tracking)
|
|
22
|
-
- [Staged Migration](#staged-migration)
|
|
23
|
-
- [Composite Attributes](#composite-attributes)
|
|
24
|
-
- [SwiftData Boundary](#swiftdata-boundary)
|
|
25
|
-
- [Testing](#testing)
|
|
26
|
-
- [Common Mistakes](#common-mistakes)
|
|
27
|
-
- [Review Checklist](#review-checklist)
|
|
28
|
-
- [References](#references)
|
|
29
|
-
|
|
30
|
-
## Stack Setup
|
|
31
|
-
|
|
32
|
-
`NSPersistentContainer` encapsulates the Core Data stack.
|
|
33
|
-
|
|
34
|
-
Docs: [NSPersistentContainer](https://sosumi.ai/documentation/coredata/nspersistentcontainer)
|
|
19
|
+
`NSPersistentContainer` wraps the model, coordinator and contexts. Create it
|
|
20
|
+
once and share it.
|
|
35
21
|
|
|
36
22
|
```swift
|
|
37
23
|
import CoreData
|
|
38
24
|
|
|
39
|
-
final class
|
|
40
|
-
static let shared =
|
|
41
|
-
|
|
25
|
+
final class LibraryStore: @unchecked Sendable {
|
|
26
|
+
static let shared = LibraryStore()
|
|
42
27
|
let container: NSPersistentContainer
|
|
43
28
|
|
|
44
29
|
private init() {
|
|
45
|
-
container = NSPersistentContainer(name: "
|
|
30
|
+
container = NSPersistentContainer(name: "Library")
|
|
46
31
|
container.loadPersistentStores { _, error in
|
|
47
|
-
if let error { fatalError("
|
|
32
|
+
if let error { fatalError("Store failed to load: \(error)") }
|
|
48
33
|
}
|
|
49
|
-
container.viewContext
|
|
50
|
-
|
|
34
|
+
let ui = container.viewContext
|
|
35
|
+
ui.automaticallyMergesChangesFromParent = true
|
|
36
|
+
ui.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump // NSMergeByPropertyObjectTrumpMergePolicy
|
|
51
37
|
}
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
func newBackgroundContext() -> NSManagedObjectContext {
|
|
39
|
+
func makeWorker() -> NSManagedObjectContext {
|
|
56
40
|
container.newBackgroundContext()
|
|
57
41
|
}
|
|
58
42
|
}
|
|
59
43
|
```
|
|
60
44
|
|
|
61
|
-
|
|
45
|
+
Replace `fatalError` with real recovery in shipping code. When the store syncs
|
|
46
|
+
through iCloud, create an `NSPersistentCloudKitContainer` instead.
|
|
62
47
|
|
|
63
|
-
##
|
|
48
|
+
## Queues and threads
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
Every context belongs to a queue: `viewContext` to the main queue, background
|
|
51
|
+
contexts to private queues.
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- Never pass `NSManagedObject` instances across context or thread boundaries.
|
|
74
|
-
Pass `NSManagedObjectID` instead and re-fetch.
|
|
75
|
-
- Set `automaticallyMergesChangesFromParent = true` on the `viewContext`.
|
|
53
|
+
- Touch a context from anywhere other than its own queue only inside
|
|
54
|
+
`perform(_:)` or `performAndWait(_:)`.
|
|
55
|
+
- Never hand an `NSManagedObject` to another context or thread. Pass its
|
|
56
|
+
`NSManagedObjectID` and fetch it again on the other side.
|
|
57
|
+
- Turn on `automaticallyMergesChangesFromParent` for `viewContext`.
|
|
76
58
|
|
|
77
59
|
```swift
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
trip.name = newName
|
|
86
|
-
try context.save()
|
|
60
|
+
func markReturned(_ loanID: NSManagedObjectID) async throws {
|
|
61
|
+
let worker = LibraryStore.shared.makeWorker()
|
|
62
|
+
try await worker.perform {
|
|
63
|
+
guard let loan = try worker.existingObject(with: loanID) as? Loan else { return }
|
|
64
|
+
loan.returnedAt = .now
|
|
65
|
+
try worker.save()
|
|
87
66
|
}
|
|
88
67
|
}
|
|
89
68
|
```
|
|
90
69
|
|
|
91
|
-
### Swift Concurrency
|
|
70
|
+
### Swift Concurrency
|
|
92
71
|
|
|
93
|
-
`
|
|
94
|
-
|
|
72
|
+
Since iOS 15 you can `try await context.perform { }`. Do not mark
|
|
73
|
+
`NSManagedObject` subclasses `Sendable` or `@unchecked Sendable`; they are not
|
|
74
|
+
thread-safe.
|
|
95
75
|
|
|
96
76
|
```swift
|
|
97
|
-
func
|
|
98
|
-
let
|
|
99
|
-
try await
|
|
100
|
-
for
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
77
|
+
func importBooks(_ rows: [BookRow]) async throws {
|
|
78
|
+
let worker = LibraryStore.shared.makeWorker()
|
|
79
|
+
try await worker.perform {
|
|
80
|
+
for row in rows {
|
|
81
|
+
let book = Book(context: worker)
|
|
82
|
+
book.isbn = row.isbn
|
|
83
|
+
book.title = row.title
|
|
104
84
|
}
|
|
105
|
-
try
|
|
85
|
+
if worker.hasChanges { try worker.save() }
|
|
106
86
|
}
|
|
107
|
-
//
|
|
87
|
+
// viewContext picks this up through automatic merging
|
|
108
88
|
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Do not use `@unchecked Sendable` on managed objects.** If you need
|
|
112
|
-
cross-boundary communication, pass the `objectID` (which is `Sendable`)
|
|
113
|
-
and re-fetch:
|
|
114
89
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
func archive(_ bookID: NSManagedObjectID) {
|
|
91
|
+
Task.detached { // NSManagedObjectID is Sendable
|
|
92
|
+
let worker = LibraryStore.shared.makeWorker()
|
|
93
|
+
try await worker.perform {
|
|
94
|
+
let book = try worker.existingObject(with: bookID) as? Book
|
|
95
|
+
book?.isArchived = true
|
|
96
|
+
try worker.save()
|
|
97
|
+
}
|
|
123
98
|
}
|
|
124
99
|
}
|
|
125
100
|
```
|
|
126
101
|
|
|
127
|
-
## NSFetchedResultsController
|
|
102
|
+
## `NSFetchedResultsController`
|
|
128
103
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Docs: [NSFetchedResultsController](https://sosumi.ai/documentation/coredata/nsfetchedresultscontroller)
|
|
104
|
+
An FRC feeds a table or collection view from a fetch request, tracks changes,
|
|
105
|
+
and can cache section data.
|
|
133
106
|
|
|
134
107
|
```swift
|
|
135
|
-
import CoreData
|
|
136
108
|
import UIKit
|
|
137
109
|
|
|
138
|
-
class
|
|
139
|
-
|
|
140
|
-
private
|
|
141
|
-
let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
|
|
142
|
-
request.sortDescriptors = [
|
|
143
|
-
NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: false)
|
|
144
|
-
]
|
|
145
|
-
request.fetchBatchSize = 20
|
|
146
|
-
|
|
147
|
-
let controller = NSFetchedResultsController(
|
|
148
|
-
fetchRequest: request,
|
|
149
|
-
managedObjectContext: CoreDataStack.shared.viewContext,
|
|
150
|
-
sectionNameKeyPath: nil,
|
|
151
|
-
cacheName: "TripsCache"
|
|
152
|
-
)
|
|
153
|
-
controller.delegate = self
|
|
154
|
-
return controller
|
|
155
|
-
}()
|
|
110
|
+
final class ShelfViewController: UICollectionViewController, @preconcurrency NSFetchedResultsControllerDelegate {
|
|
111
|
+
private var results: NSFetchedResultsController<Book>!
|
|
112
|
+
private var dataSource: UICollectionViewDiffableDataSource<String, NSManagedObjectID>!
|
|
156
113
|
|
|
157
114
|
override func viewDidLoad() {
|
|
158
115
|
super.viewDidLoad()
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
116
|
+
let request = Book.fetchRequest()
|
|
117
|
+
request.sortDescriptors = [NSSortDescriptor(keyPath: \Book.title, ascending: true)]
|
|
118
|
+
request.fetchBatchSize = 25
|
|
119
|
+
results = NSFetchedResultsController(fetchRequest: request,
|
|
120
|
+
managedObjectContext: LibraryStore.shared.container.viewContext,
|
|
121
|
+
sectionNameKeyPath: nil,
|
|
122
|
+
cacheName: nil)
|
|
123
|
+
results.delegate = self
|
|
124
|
+
try? results.performFetch()
|
|
166
125
|
}
|
|
167
126
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
173
|
-
let cell = tableView.dequeueReusableCell(withIdentifier: "TripCell", for: indexPath)
|
|
174
|
-
let trip = fetchedResultsController.object(at: indexPath)
|
|
175
|
-
cell.textLabel?.text = trip.name
|
|
176
|
-
return cell
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// MARK: - NSFetchedResultsControllerDelegate (diffable)
|
|
180
|
-
|
|
181
|
-
func controller(
|
|
182
|
-
_ controller: NSFetchedResultsController<any NSFetchRequestResult>,
|
|
183
|
-
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
|
|
184
|
-
) {
|
|
185
|
-
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
|
186
|
-
dataSource.apply(snapshot, animatingDifferences: true)
|
|
127
|
+
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
|
128
|
+
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
|
129
|
+
let typed = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
|
130
|
+
dataSource.apply(typed, animatingDifferences: true)
|
|
187
131
|
}
|
|
188
132
|
}
|
|
189
133
|
```
|
|
190
134
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- Call `deleteCache(withName:)` before changing the fetch request predicate or
|
|
194
|
-
sort descriptors, or set `cacheName` to `nil`.
|
|
195
|
-
- The diffable snapshot delegate method (`didChangeContentWith:`) is available
|
|
196
|
-
iOS 13+ and is preferred over the older per-change callbacks.
|
|
197
|
-
- After a context `reset()`, call `performFetch()` again.
|
|
135
|
+
Without a diffable data source, read `results.sections?.count`,
|
|
136
|
+
`results.sections?[section].numberOfObjects` and `results.object(at:)`.
|
|
198
137
|
|
|
199
|
-
|
|
138
|
+
Rules:
|
|
200
139
|
|
|
201
|
-
|
|
202
|
-
|
|
140
|
+
- The fetch request needs at least one sort descriptor.
|
|
141
|
+
- Before changing the predicate or sort descriptors, call
|
|
142
|
+
`NSFetchedResultsController.deleteCache(withName:)` or use `cacheName: nil`.
|
|
143
|
+
- The snapshot callback (iOS 13+) is preferred over the per-object change callbacks.
|
|
144
|
+
- After `reset()` on the context, call `performFetch()` again.
|
|
203
145
|
|
|
204
|
-
|
|
146
|
+
## Batch requests
|
|
205
147
|
|
|
206
|
-
|
|
148
|
+
Batch requests run directly against SQLite. They skip the context, which makes
|
|
149
|
+
them fast, but they post no context notifications, so **merge their results
|
|
150
|
+
yourself**.
|
|
207
151
|
|
|
208
152
|
```swift
|
|
209
|
-
func
|
|
210
|
-
let
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
entity: CDTrip.entity(),
|
|
214
|
-
objects: records
|
|
215
|
-
)
|
|
216
|
-
request.resultType = .objectIDs
|
|
217
|
-
let result = try context.execute(request) as? NSBatchInsertResult
|
|
218
|
-
if let ids = result?.result as? [NSManagedObjectID] {
|
|
219
|
-
NSManagedObjectContext.mergeChanges(
|
|
220
|
-
fromRemoteContextSave: [NSInsertedObjectsKey: ids],
|
|
221
|
-
into: [CoreDataStack.shared.viewContext]
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
153
|
+
@Sendable func publish(_ result: Any?, as key: String) {
|
|
154
|
+
let ids = result as? [NSManagedObjectID] ?? []
|
|
155
|
+
let ui = LibraryStore.shared.container.viewContext
|
|
156
|
+
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [key: ids], into: [ui])
|
|
225
157
|
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### NSBatchDeleteRequest (iOS 9+)
|
|
229
158
|
|
|
230
|
-
|
|
159
|
+
let worker = LibraryStore.shared.makeWorker()
|
|
231
160
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
|
239
|
-
request.resultType = .resultTypeObjectIDs
|
|
240
|
-
let result = try context.execute(request) as? NSBatchDeleteResult
|
|
241
|
-
if let ids = result?.result as? [NSManagedObjectID] {
|
|
242
|
-
NSManagedObjectContext.mergeChanges(
|
|
243
|
-
fromRemoteContextSave: [NSDeletedObjectsKey: ids],
|
|
244
|
-
into: [CoreDataStack.shared.viewContext]
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
161
|
+
// Insert (iOS 13+)
|
|
162
|
+
try await worker.perform {
|
|
163
|
+
let insert = NSBatchInsertRequest(entity: Book.entity(), objects: rows.map { ["isbn": $0.isbn, "title": $0.title] })
|
|
164
|
+
insert.resultType = .objectIDs
|
|
165
|
+
let inserted = try worker.execute(insert) as? NSBatchInsertResult
|
|
166
|
+
publish(inserted?.result, as: NSInsertedObjectsKey)
|
|
248
167
|
}
|
|
249
|
-
```
|
|
250
168
|
|
|
251
|
-
|
|
169
|
+
// Delete (iOS 9+)
|
|
170
|
+
try await worker.perform {
|
|
171
|
+
let stale: NSFetchRequest<NSFetchRequestResult> = Loan.fetchRequest()
|
|
172
|
+
stale.predicate = NSPredicate(format: "returnedAt < %@", cutoff as NSDate)
|
|
173
|
+
let delete = NSBatchDeleteRequest(fetchRequest: stale)
|
|
174
|
+
delete.resultType = .resultTypeObjectIDs
|
|
175
|
+
let removed = try worker.execute(delete) as? NSBatchDeleteResult
|
|
176
|
+
publish(removed?.result, as: NSDeletedObjectsKey)
|
|
177
|
+
}
|
|
252
178
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
let result = try context.execute(request) as? NSBatchUpdateResult
|
|
261
|
-
if let ids = result?.result as? [NSManagedObjectID] {
|
|
262
|
-
NSManagedObjectContext.mergeChanges(
|
|
263
|
-
fromRemoteContextSave: [NSUpdatedObjectsKey: ids],
|
|
264
|
-
into: [CoreDataStack.shared.viewContext]
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
179
|
+
// Update (iOS 8+)
|
|
180
|
+
try await worker.perform {
|
|
181
|
+
let update = NSBatchUpdateRequest(entity: Book.entity())
|
|
182
|
+
update.propertiesToUpdate = ["isArchived": true]
|
|
183
|
+
update.resultType = .updatedObjectIDsResultType
|
|
184
|
+
let changed = try worker.execute(update) as? NSBatchUpdateResult
|
|
185
|
+
publish(changed?.result, as: NSUpdatedObjectsKey)
|
|
268
186
|
}
|
|
269
187
|
```
|
|
270
188
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
## Persistent History Tracking
|
|
189
|
+
A batch delete does not honor the **Deny** delete rule and skips validation
|
|
190
|
+
beyond the basic delete rules. Check those constraints yourself first.
|
|
275
191
|
|
|
276
|
-
|
|
277
|
-
processes.
|
|
192
|
+
## Persistent history
|
|
278
193
|
|
|
279
|
-
|
|
194
|
+
History tracking records store-level changes so the app, its extensions and
|
|
195
|
+
widgets (separate processes) can see each other's writes, including batch
|
|
196
|
+
requests.
|
|
280
197
|
|
|
281
|
-
###
|
|
198
|
+
### Turn it on
|
|
282
199
|
|
|
283
200
|
```swift
|
|
284
|
-
let description =
|
|
285
|
-
|
|
286
|
-
description.setOption(true
|
|
287
|
-
|
|
201
|
+
guard let description = container.persistentStoreDescriptions.first else { return }
|
|
202
|
+
for key in [NSPersistentHistoryTrackingKey, NSPersistentStoreRemoteChangeNotificationPostOptionKey] {
|
|
203
|
+
description.setOption(NSNumber(value: true), forKey: key)
|
|
204
|
+
}
|
|
288
205
|
container.persistentStoreDescriptions = [description]
|
|
289
206
|
```
|
|
290
207
|
|
|
291
|
-
###
|
|
208
|
+
### Consume it
|
|
209
|
+
|
|
210
|
+
1. Observe `.NSPersistentStoreRemoteChange`, with the container's persistent store coordinator as the object.
|
|
211
|
+
2. On a background context, fetch transactions after your last token.
|
|
212
|
+
3. Merge each transaction into `viewContext` and move the token forward.
|
|
213
|
+
4. Purge history older than the token.
|
|
292
214
|
|
|
293
215
|
```swift
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
216
|
+
final class HistoryReader: NSObject, @unchecked Sendable {
|
|
217
|
+
private let container: NSPersistentContainer
|
|
218
|
+
private let tokenKey: String // one per target: "history.app", "history.widget"
|
|
219
|
+
private var lastToken: NSPersistentHistoryToken?
|
|
220
|
+
|
|
221
|
+
init(container: NSPersistentContainer, target: String) {
|
|
222
|
+
self.container = container
|
|
223
|
+
self.tokenKey = "history.\(target)"
|
|
224
|
+
super.init()
|
|
225
|
+
lastToken = Self.loadToken(forKey: tokenKey)
|
|
226
|
+
let center = NotificationCenter.default
|
|
227
|
+
center.addObserver(self, selector: #selector(storeChanged),
|
|
228
|
+
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@objc private func storeChanged(_ note: Notification) {
|
|
232
|
+
let worker = container.newBackgroundContext()
|
|
233
|
+
worker.perform { [self] in
|
|
234
|
+
let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
|
|
235
|
+
let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
|
|
236
|
+
guard let transactions = history?.result as? [NSPersistentHistoryTransaction] else { return }
|
|
237
|
+
let ui = container.viewContext
|
|
308
238
|
for transaction in transactions {
|
|
309
|
-
|
|
310
|
-
|
|
239
|
+
let changes = transaction.objectIDNotification().userInfo ?? [:]
|
|
240
|
+
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [ui])
|
|
241
|
+
lastToken = transaction.token
|
|
242
|
+
}
|
|
243
|
+
Self.saveToken(lastToken, forKey: tokenKey)
|
|
244
|
+
if let lastToken {
|
|
245
|
+
_ = try? worker.execute(NSPersistentHistoryChangeRequest.deleteHistory(before: lastToken))
|
|
311
246
|
}
|
|
312
247
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private static var defaults: UserDefaults { UserDefaults(suiteName: "group.app.library") ?? .standard }
|
|
251
|
+
|
|
252
|
+
private static func loadToken(forKey key: String) -> NSPersistentHistoryToken? {
|
|
253
|
+
defaults.data(forKey: key).flatMap {
|
|
254
|
+
try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: $0)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private static func saveToken(_ token: NSPersistentHistoryToken?, forKey key: String) {
|
|
259
|
+
guard let token else { return }
|
|
260
|
+
let encoded = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
|
|
261
|
+
defaults.set(encoded, forKey: key)
|
|
316
262
|
}
|
|
317
263
|
}
|
|
318
264
|
```
|
|
319
265
|
|
|
320
|
-
|
|
321
|
-
|
|
266
|
+
The static merge call is safe from the worker queue; calling
|
|
267
|
+
`viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())`
|
|
268
|
+
inside `viewContext.perform` is the equivalent instance form.
|
|
322
269
|
|
|
323
|
-
|
|
270
|
+
Keep one token per target (for example in the shared `UserDefaults` suite) so
|
|
271
|
+
each process resumes where it stopped after relaunch. Purge only up to the
|
|
272
|
+
point every reader has consumed.
|
|
324
273
|
|
|
325
|
-
|
|
326
|
-
ordered stages, each lightweight or custom.
|
|
274
|
+
## Staged migration (iOS 17+)
|
|
327
275
|
|
|
328
|
-
|
|
276
|
+
`NSStagedMigrationManager` applies an ordered list of stages, each either
|
|
277
|
+
lightweight or custom.
|
|
329
278
|
|
|
330
|
-
|
|
331
|
-
|
|
279
|
+
- `NSLightweightMigrationStage` takes the **version checksums** (`[String]`) of
|
|
280
|
+
compiled model versions, not their names, and has a `label`.
|
|
281
|
+
- `NSManagedObjectModelReference(name:in:versionChecksum:)` points to one model version.
|
|
282
|
+
- `NSCustomMigrationStage(migratingFrom:to:)` takes a source and a destination
|
|
283
|
+
reference and offers `label` and `willMigrateHandler` (called with the manager and stage).
|
|
332
284
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
let
|
|
336
|
-
let
|
|
337
|
-
|
|
338
|
-
let
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
in: Bundle.main,
|
|
344
|
-
versionChecksum: checksumV2
|
|
345
|
-
)
|
|
346
|
-
let modelV3 = NSManagedObjectModelReference(
|
|
347
|
-
name: "ModelV3",
|
|
348
|
-
in: Bundle.main,
|
|
349
|
-
versionChecksum: checksumV3
|
|
350
|
-
)
|
|
351
|
-
let stage2to3 = NSCustomMigrationStage(
|
|
352
|
-
migratingFrom: modelV2,
|
|
353
|
-
to: modelV3
|
|
354
|
-
)
|
|
355
|
-
stage2to3.label = "Split name into firstName/lastName"
|
|
356
|
-
stage2to3.willMigrateHandler = { migrationManager, currentStage in
|
|
357
|
-
guard let container = migrationManager.container else { return }
|
|
358
|
-
let context = container.newBackgroundContext()
|
|
285
|
+
```swift
|
|
286
|
+
let v1 = NSManagedObjectModelReference(name: "Library_v1", in: .main, versionChecksum: "k3Qp...v1")
|
|
287
|
+
let v2 = NSManagedObjectModelReference(name: "Library_v2", in: .main, versionChecksum: "R8bz...v2")
|
|
288
|
+
let v3Checksum = "Yt5m...v3"
|
|
289
|
+
|
|
290
|
+
let splitAuthor = NSCustomMigrationStage(migratingFrom: v1, to: v2)
|
|
291
|
+
splitAuthor.label = "Split author into first and last name"
|
|
292
|
+
splitAuthor.willMigrateHandler = { manager, _ in
|
|
293
|
+
guard let store = manager.container else { return }
|
|
294
|
+
let context = store.newBackgroundContext()
|
|
359
295
|
try context.performAndWait {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
person.setValue(String(parts.first ?? ""), forKey: "firstName")
|
|
367
|
-
person.setValue(parts.count > 1 ? String(parts.last!) : "", forKey: "lastName")
|
|
296
|
+
let request = NSFetchRequest<NSManagedObject>(entityName: "Book")
|
|
297
|
+
for book in try context.fetch(request) {
|
|
298
|
+
let full = book.value(forKey: "author") as? String ?? ""
|
|
299
|
+
let parts = full.split(separator: " ", maxSplits: 1).map(String.init)
|
|
300
|
+
book.setValue(parts.first, forKey: "authorFirst")
|
|
301
|
+
book.setValue(parts.count > 1 ? parts[1] : nil, forKey: "authorLast")
|
|
368
302
|
}
|
|
369
303
|
try context.save()
|
|
370
304
|
}
|
|
371
305
|
}
|
|
372
306
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
307
|
+
let addIndexes = NSLightweightMigrationStage([v3Checksum])
|
|
308
|
+
addIndexes.label = "v2 to v3: add indexes"
|
|
309
|
+
|
|
310
|
+
let manager = NSStagedMigrationManager([splitAuthor, addIndexes])
|
|
311
|
+
let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
|
|
312
|
+
description.setOption(manager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
|
|
378
313
|
container.persistentStoreDescriptions = [description]
|
|
379
314
|
container.loadPersistentStores { _, error in
|
|
380
|
-
if let error {
|
|
315
|
+
if let error { MigrationLog.failed(error) }
|
|
381
316
|
}
|
|
382
317
|
```
|
|
383
318
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
`NSLightweightMigrationStage` takes **version checksums** (`[String]`), not
|
|
388
|
-
human-readable model names.
|
|
389
|
-
|
|
390
|
-
## Composite Attributes
|
|
391
|
-
|
|
392
|
-
iOS 17+ supports composite attributes: groups of sub-attributes on an entity
|
|
393
|
-
that act as a single logical unit. Define them in the model editor by adding a
|
|
394
|
-
Composite type attribute and nesting sub-attributes beneath it.
|
|
319
|
+
The option must be on the store description **before** `loadPersistentStores`.
|
|
320
|
+
Read the checksums from the compiled `.momd` (for example
|
|
321
|
+
`NSManagedObjectModel.versionChecksum`), never type a model name in their place.
|
|
395
322
|
|
|
396
|
-
|
|
323
|
+
Older deployment targets fall back to inferred mapping
|
|
324
|
+
(`NSInferMappingModelAutomaticallyOption`) or hand-written mapping models.
|
|
397
325
|
|
|
398
|
-
Composite attributes
|
|
399
|
-
scenarios.
|
|
326
|
+
## Composite attributes (iOS 17+)
|
|
400
327
|
|
|
401
|
-
|
|
328
|
+
`NSCompositeAttributeDescription` groups several sub-attributes into one
|
|
329
|
+
logical attribute, for example an address made of street, city and postcode.
|
|
330
|
+
Create it in the model editor by choosing the **Composite** type and adding
|
|
331
|
+
nested attributes. When the same store is read by SwiftData, a composite maps
|
|
332
|
+
to a `Codable` struct.
|
|
402
333
|
|
|
403
|
-
|
|
404
|
-
implementation. Before handing off, preserve these Core Data boundaries:
|
|
334
|
+
## Where SwiftData starts
|
|
405
335
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
- Shared persisted data must keep entity names, property names, types, and
|
|
409
|
-
schema compatible across the Core Data model and SwiftData `@Model` classes.
|
|
410
|
-
- Map renamed persisted properties with SwiftData `@Attribute(originalName:)`.
|
|
336
|
+
Coexistence and migration work belongs to `swiftdata`. The rules that matter
|
|
337
|
+
from this side:
|
|
411
338
|
|
|
412
|
-
|
|
339
|
+
- Point SwiftData at the **existing** store URL when it shares or takes over Core Data data.
|
|
340
|
+
- The `@Model` classes must line up with the Core Data model: same entity and
|
|
341
|
+
property names, matching types, a compatible schema.
|
|
342
|
+
- Map renamed persisted properties with `@Attribute(originalName:)`.
|
|
413
343
|
|
|
414
|
-
|
|
344
|
+
## Testing with an in-memory store
|
|
415
345
|
|
|
416
346
|
```swift
|
|
417
|
-
import CoreData
|
|
418
347
|
import Testing
|
|
348
|
+
import CoreData
|
|
419
349
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
let
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
var loadError: Error?
|
|
428
|
-
container.loadPersistentStores { _, error in loadError = error }
|
|
429
|
-
if let loadError { throw loadError }
|
|
430
|
-
return container
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
@Test func createAndFetchTrip() throws {
|
|
434
|
-
let container = try makeTestContainer()
|
|
435
|
-
let context = container.viewContext
|
|
436
|
-
|
|
437
|
-
let trip = CDTrip(context: context)
|
|
438
|
-
trip.name = "Test Trip"
|
|
439
|
-
trip.startDate = .now
|
|
440
|
-
try context.save()
|
|
350
|
+
enum TestModel {
|
|
351
|
+
nonisolated(unsafe) static let shared: NSManagedObjectModel = { // loaded once, never mutated
|
|
352
|
+
let compiled = Bundle(for: LibraryStore.self).url(forResource: "Library", withExtension: "momd")
|
|
353
|
+
return compiled.flatMap(NSManagedObjectModel.init(contentsOf:)) ?? NSManagedObjectModel()
|
|
354
|
+
}()
|
|
355
|
+
}
|
|
441
356
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
357
|
+
func makeMemoryContainer() throws -> NSPersistentContainer {
|
|
358
|
+
let container = NSPersistentContainer(name: "Library", managedObjectModel: TestModel.shared)
|
|
359
|
+
let memory = NSPersistentStoreDescription()
|
|
360
|
+
memory.type = NSInMemoryStoreType
|
|
361
|
+
container.persistentStoreDescriptions = [memory]
|
|
362
|
+
var failure: Error?
|
|
363
|
+
container.loadPersistentStores { _, problem in failure = problem }
|
|
364
|
+
if let failure { throw failure }
|
|
365
|
+
return container
|
|
447
366
|
}
|
|
448
|
-
```
|
|
449
367
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
368
|
+
@Test func savingABookPersistsIt() throws {
|
|
369
|
+
let context = try makeMemoryContainer().viewContext
|
|
370
|
+
let book = Book(context: context)
|
|
371
|
+
book.title = "Dune"
|
|
372
|
+
try context.save()
|
|
454
373
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return NSManagedObjectModel(contentsOf: url)!
|
|
459
|
-
}()
|
|
460
|
-
|
|
461
|
-
func makeTestContainer() throws -> NSPersistentContainer {
|
|
462
|
-
let container = NSPersistentContainer(name: "MyAppModel",
|
|
463
|
-
managedObjectModel: sharedModel)
|
|
464
|
-
// ... configure in-memory store
|
|
374
|
+
let fetched = try context.fetch(Book.fetchRequest())
|
|
375
|
+
#expect(fetched.count == 1)
|
|
376
|
+
#expect(fetched.first?.title == "Dune")
|
|
465
377
|
}
|
|
466
378
|
```
|
|
467
379
|
|
|
468
|
-
|
|
380
|
+
Load the model once and pass the same `NSManagedObjectModel` to every test
|
|
381
|
+
container; loading it per test produces "multiple entity descriptions claim"
|
|
382
|
+
duplicate-entity warnings.
|
|
383
|
+
|
|
384
|
+
## Common mistakes
|
|
469
385
|
|
|
470
386
|
| Mistake | Fix |
|
|
471
|
-
|
|
472
|
-
|
|
|
473
|
-
|
|
|
474
|
-
|
|
|
475
|
-
|
|
|
476
|
-
|
|
|
477
|
-
|
|
|
478
|
-
|
|
|
479
|
-
|
|
|
480
|
-
|
|
|
481
|
-
|
|
482
|
-
## Review
|
|
483
|
-
|
|
484
|
-
- [ ] `NSPersistentContainer
|
|
485
|
-
- [ ] `viewContext` used only on main queue; background contexts
|
|
486
|
-
- [ ] `perform
|
|
487
|
-
- [ ] `automaticallyMergesChangesFromParent`
|
|
488
|
-
- [ ] `mergePolicy`
|
|
489
|
-
- [ ] Batch
|
|
490
|
-
- [ ]
|
|
491
|
-
- [ ] Persistent history
|
|
492
|
-
- [ ]
|
|
493
|
-
- [ ]
|
|
494
|
-
- [ ] No `NSManagedObject`
|
|
387
|
+
|---|---|
|
|
388
|
+
| Handing an `NSManagedObject` to another thread | Send `objectID` and fetch again |
|
|
389
|
+
| Batch results never merged | `NSManagedObjectContext.mergeChanges(fromRemoteContextSave:into:)` |
|
|
390
|
+
| `save()` with nothing to save | Check `context.hasChanges` first |
|
|
391
|
+
| The deprecated confinement type passed to `init(concurrencyType:)` | Pick `.privateQueueConcurrencyType` or `.mainQueueConcurrencyType` |
|
|
392
|
+
| No `mergePolicy` on `viewContext` | `NSMergeByPropertyObjectTrumpMergePolicy`, avoiding merge-conflict failures |
|
|
393
|
+
| Editing a live FRC request without clearing its cache | `deleteCache(withName:)` or `cacheName: nil` |
|
|
394
|
+
| Relying on the Deny rule with batch delete | Batch delete ignores it; validate manually |
|
|
395
|
+
| `@unchecked Sendable` on managed objects | Remove it; pass `objectID` |
|
|
396
|
+
| SwiftData opening a fresh, empty file while sharing data with Core Data | Reuse the current store URL and keep the schema compatible |
|
|
397
|
+
|
|
398
|
+
## Review checklist
|
|
399
|
+
|
|
400
|
+
- [ ] One `NSPersistentContainer`, created once and shared
|
|
401
|
+
- [ ] `viewContext` used only on the main queue; writes on background contexts
|
|
402
|
+
- [ ] Every off-queue access wrapped in `perform` or `performAndWait`
|
|
403
|
+
- [ ] `automaticallyMergesChangesFromParent` on `viewContext`
|
|
404
|
+
- [ ] `mergePolicy` on `viewContext`
|
|
405
|
+
- [ ] Batch request results merged
|
|
406
|
+
- [ ] FRC fetch requests sorted
|
|
407
|
+
- [ ] Persistent history enabled when several targets share the store
|
|
408
|
+
- [ ] SwiftData hand-off keeps the store URL, schema, entity and property names, and rename mappings
|
|
409
|
+
- [ ] Test containers are in-memory and share a single loaded model
|
|
410
|
+
- [ ] No `NSManagedObject` crosses a thread boundary
|
|
495
411
|
|
|
496
412
|
## References
|
|
497
413
|
|
|
498
|
-
-
|
|
414
|
+
- [Core Data](https://developer.apple.com/documentation/coredata)
|
|
415
|
+
- [NSPersistentContainer](https://developer.apple.com/documentation/coredata/nspersistentcontainer)
|
|
416
|
+
- [NSManagedObjectContext](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)
|
|
417
|
+
- [NSFetchedResultsController](https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller)
|
|
418
|
+
- [NSBatchInsertRequest](https://developer.apple.com/documentation/coredata/nsbatchinsertrequest)
|
|
419
|
+
- [NSBatchDeleteRequest](https://developer.apple.com/documentation/coredata/nsbatchdeleterequest)
|
|
420
|
+
- [NSPersistentHistoryChangeRequest](https://developer.apple.com/documentation/coredata/nspersistenthistorychangerequest)
|
|
421
|
+
- [NSStagedMigrationManager](https://developer.apple.com/documentation/coredata/nsstagedmigrationmanager)
|
|
422
|
+
- [NSCompositeAttributeDescription](https://developer.apple.com/documentation/coredata/nscompositeattributedescription)
|