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