@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,976 +1,583 @@
|
|
|
1
|
-
# SwiftData
|
|
2
|
-
|
|
3
|
-
Deep reference for custom data stores, history tracking, CloudKit integration,
|
|
4
|
-
Core Data coexistence, batch operations, complex predicates, composite
|
|
5
|
-
attributes, model inheritance, multiple containers, undo/redo, and preview
|
|
6
|
-
patterns.
|
|
7
|
-
|
|
8
|
-
---
|
|
1
|
+
# SwiftData: advanced topics
|
|
9
2
|
|
|
10
3
|
## Contents
|
|
11
4
|
|
|
12
|
-
- [Custom
|
|
13
|
-
- [History
|
|
14
|
-
- [CloudKit
|
|
15
|
-
- [Core Data
|
|
16
|
-
- [Batch
|
|
17
|
-
- [Complex
|
|
18
|
-
- [
|
|
19
|
-
- [Model
|
|
20
|
-
- [
|
|
21
|
-
- [Undo
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
5
|
+
- [Custom data stores](#custom-data-stores)
|
|
6
|
+
- [History tracking](#history-tracking)
|
|
7
|
+
- [CloudKit in detail](#cloudkit-in-detail)
|
|
8
|
+
- [Core Data strategies](#core-data-strategies)
|
|
9
|
+
- [Batch work and performance](#batch-work-and-performance)
|
|
10
|
+
- [Complex predicates](#complex-predicates)
|
|
11
|
+
- [Codable structs as composite attributes](#codable-structs-as-composite-attributes)
|
|
12
|
+
- [Model inheritance](#model-inheritance)
|
|
13
|
+
- [Several configurations in one container](#several-configurations-in-one-container)
|
|
14
|
+
- [Undo and redo](#undo-and-redo)
|
|
15
|
+
- [Previews with in-memory stores](#previews-with-in-memory-stores)
|
|
16
|
+
- [Save notifications](#save-notifications)
|
|
17
|
+
- [Errors](#errors)
|
|
25
18
|
|
|
26
|
-
## Custom
|
|
19
|
+
## Custom data stores
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
From iOS 18 you can replace the SQLite backend by conforming to `DataStore`:
|
|
22
|
+
a JSON file, a cache, a REST service, anything that can answer fetches and
|
|
23
|
+
accept saves.
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
with a custom persistence backend (JSON files, in-memory caches, REST APIs,
|
|
32
|
-
etc.).
|
|
25
|
+
Shape of a store:
|
|
33
26
|
|
|
34
27
|
```swift
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
import SwiftData
|
|
29
|
+
import Foundation
|
|
30
|
+
|
|
31
|
+
struct ArchiveStoreConfiguration: DataStoreConfiguration {
|
|
32
|
+
typealias Store = ArchiveStore
|
|
33
|
+
var name: String
|
|
34
|
+
var schema: Schema?
|
|
35
|
+
let fileURL: URL
|
|
36
|
+
|
|
37
|
+
func validate() throws {
|
|
38
|
+
guard fileURL.isFileURL else { throw DataStoreError.unsupportedFeature }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
final class ArchiveStore: DataStore {
|
|
43
|
+
typealias Configuration = ArchiveStoreConfiguration
|
|
37
44
|
typealias Snapshot = DefaultSnapshot
|
|
38
45
|
|
|
39
|
-
let configuration:
|
|
46
|
+
let configuration: ArchiveStoreConfiguration
|
|
40
47
|
let identifier: String
|
|
41
48
|
let schema: Schema
|
|
42
49
|
|
|
43
|
-
init(_ configuration:
|
|
50
|
+
init(_ configuration: ArchiveStoreConfiguration,
|
|
44
51
|
migrationPlan: (any SchemaMigrationPlan.Type)?) throws {
|
|
45
52
|
self.configuration = configuration
|
|
46
53
|
self.identifier = configuration.name
|
|
47
54
|
self.schema = configuration.schema ?? Schema()
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
func fetch<T:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
descriptor: request.descriptor,
|
|
57
|
-
fetchedSnapshots: snapshots,
|
|
58
|
-
relatedSnapshots: [:]
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
func fetchCount<T: PersistentModel>(
|
|
63
|
-
_ request: DataStoreFetchRequest<T>
|
|
64
|
-
) throws -> Int {
|
|
65
|
-
try fetch(request).fetchedSnapshots.count
|
|
57
|
+
func fetch<T>(_ request: DataStoreFetchRequest<T>) throws
|
|
58
|
+
-> DataStoreFetchResult<T, Snapshot> where T: PersistentModel {
|
|
59
|
+
let snapshots: [DefaultSnapshot] = [] // read from the archive, apply request.descriptor
|
|
60
|
+
return DataStoreFetchResult(descriptor: request.descriptor,
|
|
61
|
+
fetchedSnapshots: snapshots,
|
|
62
|
+
relatedSnapshots: [:])
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
func
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
func save(
|
|
75
|
-
_ request: DataStoreSaveChangesRequest<DefaultSnapshot>
|
|
76
|
-
) throws -> DataStoreSaveChangesResult<DefaultSnapshot> {
|
|
77
|
-
// Persist inserted, updated; remove deleted
|
|
78
|
-
return DataStoreSaveChangesResult(
|
|
79
|
-
for: identifier,
|
|
80
|
-
remappedIdentifiers: [:],
|
|
81
|
-
snapshotsToReregister: [:]
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
func erase() throws {
|
|
86
|
-
// Remove all persisted data
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func initializeState(for editingState: EditingState) {}
|
|
90
|
-
func invalidateState(for editingState: EditingState) {}
|
|
91
|
-
|
|
92
|
-
func cachedSnapshots(
|
|
93
|
-
for identifiers: [PersistentIdentifier],
|
|
94
|
-
editingState: EditingState
|
|
95
|
-
) throws -> [PersistentIdentifier: DefaultSnapshot] {
|
|
96
|
-
[:]
|
|
65
|
+
func save(_ request: DataStoreSaveChangesRequest<Snapshot>) throws
|
|
66
|
+
-> DataStoreSaveChangesResult<Snapshot> {
|
|
67
|
+
// write request.inserted, request.updated, request.deleted
|
|
68
|
+
DataStoreSaveChangesResult(for: identifier,
|
|
69
|
+
remappedIdentifiers: [:],
|
|
70
|
+
snapshotsToReregister: [:])
|
|
97
71
|
}
|
|
98
72
|
}
|
|
99
73
|
```
|
|
100
74
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
struct JSONStoreConfiguration: DataStoreConfiguration {
|
|
105
|
-
typealias Store = JSONStore
|
|
106
|
-
|
|
107
|
-
let name: String
|
|
108
|
-
var schema: Schema?
|
|
109
|
-
let fileURL: URL
|
|
110
|
-
|
|
111
|
-
init(name: String, fileURL: URL) {
|
|
112
|
-
self.name = name
|
|
113
|
-
self.fileURL = fileURL
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
func validate() throws {
|
|
117
|
-
// Validate file URL is accessible
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
75
|
+
The sketch shows the two central members. The protocol also requires
|
|
76
|
+
`fetchCount`, `fetchIdentifiers`, `erase()`, `initializeState(for: EditingState)`,
|
|
77
|
+
`invalidateState(for:)` and `cachedSnapshots(for:editingState:)`.
|
|
121
78
|
|
|
122
|
-
|
|
79
|
+
Use it like any other configuration:
|
|
123
80
|
|
|
124
81
|
```swift
|
|
125
|
-
let
|
|
126
|
-
|
|
127
|
-
fileURL: URL.documentsDirectory.appending(path: "data.json")
|
|
128
|
-
)
|
|
129
|
-
let container = try ModelContainer(
|
|
130
|
-
for: Trip.self,
|
|
131
|
-
configurations: config
|
|
132
|
-
)
|
|
82
|
+
let archive = ArchiveStoreConfiguration(name: "Archive", schema: nil, fileURL: archiveURL)
|
|
83
|
+
let archivedNotes = try ModelContainer(for: ArchivedNote.self, configurations: archive)
|
|
133
84
|
```
|
|
134
85
|
|
|
135
|
-
|
|
86
|
+
Optional extras:
|
|
136
87
|
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
for change tracking.
|
|
88
|
+
- `DataStoreBatching` adds `delete(_:)` for batch deletes.
|
|
89
|
+
- `HistoryProviding` adds `fetchHistory(_:)` and `deleteHistory(_:)`.
|
|
140
90
|
|
|
141
|
-
|
|
91
|
+
When a request is beyond the store, throw a `DataStoreError`:
|
|
92
|
+
`.invalidPredicate`, `.preferInMemoryFilter` (SwiftData filters in memory
|
|
93
|
+
instead), `.preferInMemorySort`, or `.unsupportedFeature`.
|
|
142
94
|
|
|
143
|
-
|
|
95
|
+
## History tracking
|
|
144
96
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
| `.preferInMemoryFilter` | Store cannot filter; framework filters in memory |
|
|
149
|
-
| `.preferInMemorySort` | Store cannot sort; framework sorts in memory |
|
|
150
|
-
| `.unsupportedFeature` | Store does not support the requested operation |
|
|
97
|
+
From iOS 18 the default store records a history of transactions, so an app can
|
|
98
|
+
catch up on what changed since it last looked, including changes made by
|
|
99
|
+
widgets and extensions.
|
|
151
100
|
|
|
152
|
-
|
|
101
|
+
Label your own writes:
|
|
153
102
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
103
|
+
```swift
|
|
104
|
+
context.author = "main-app"
|
|
105
|
+
```
|
|
157
106
|
|
|
158
|
-
|
|
159
|
-
Mark attributes with `.preserveValueOnDeletion` to retain values in tombstones
|
|
160
|
-
after deletion.
|
|
107
|
+
Mark attributes whose values should survive into deletion records:
|
|
161
108
|
|
|
162
109
|
```swift
|
|
163
110
|
@Model
|
|
164
|
-
class
|
|
165
|
-
@Attribute(.preserveValueOnDeletion) var
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
init(name: String, destination: String, startDate: Date) {
|
|
170
|
-
self.name = name
|
|
171
|
-
self.destination = destination
|
|
172
|
-
self.startDate = startDate
|
|
173
|
-
}
|
|
111
|
+
final class Receipt {
|
|
112
|
+
@Attribute(.preserveValueOnDeletion) var number: String
|
|
113
|
+
var amount: Decimal
|
|
114
|
+
init(number: String, amount: Decimal) { self.number = number; self.amount = amount }
|
|
174
115
|
}
|
|
175
|
-
|
|
176
|
-
// Tag context for history attribution
|
|
177
|
-
modelContext.author = "mainApp"
|
|
178
116
|
```
|
|
179
117
|
|
|
180
|
-
|
|
118
|
+
Read changes since a saved token. `mirror` stands in for whatever your app does
|
|
119
|
+
with each change (refresh a cache, update a widget, push to a server). The
|
|
120
|
+
`.update` and `.delete` payloads are existentials, so cast them to
|
|
121
|
+
`DefaultHistoryUpdate<Model>` / `DefaultHistoryDelete<Model>` before reading
|
|
122
|
+
`updatedAttributes` or `tombstone`; a tombstone value comes back as
|
|
123
|
+
`(any Sendable)?`:
|
|
181
124
|
|
|
182
125
|
```swift
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
126
|
+
func applyChanges(since lastToken: DefaultHistoryToken?,
|
|
127
|
+
in context: ModelContext,
|
|
128
|
+
mirror: HistoryMirror) throws -> DefaultHistoryToken? {
|
|
129
|
+
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
|
|
130
|
+
if let lastToken {
|
|
131
|
+
descriptor.predicate = #Predicate { $0.token > lastToken }
|
|
189
132
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
case .delete(let delete):
|
|
210
|
-
let deletedID = delete.changedPersistentIdentifier
|
|
211
|
-
let tombstone = delete.tombstone
|
|
212
|
-
// Access preserved values
|
|
213
|
-
if let name = tombstone[\.name] as? String {
|
|
214
|
-
// Use preserved name for sync/audit
|
|
133
|
+
let transactions = try context.fetchHistory(descriptor)
|
|
134
|
+
for transaction in transactions {
|
|
135
|
+
for change in transaction.changes {
|
|
136
|
+
switch change {
|
|
137
|
+
case .insert(let inserted):
|
|
138
|
+
mirror.added(inserted.changedPersistentIdentifier)
|
|
139
|
+
case .update(let updated):
|
|
140
|
+
if let receipt = updated as? DefaultHistoryUpdate<Receipt> {
|
|
141
|
+
mirror.changed(receipt.changedPersistentIdentifier,
|
|
142
|
+
fields: receipt.updatedAttributes)
|
|
143
|
+
}
|
|
144
|
+
case .delete(let deleted):
|
|
145
|
+
if let receipt = deleted as? DefaultHistoryDelete<Receipt> {
|
|
146
|
+
mirror.removed(receipt.changedPersistentIdentifier,
|
|
147
|
+
number: receipt.tombstone[\.number] as? String)
|
|
148
|
+
}
|
|
149
|
+
@unknown default:
|
|
150
|
+
break
|
|
215
151
|
}
|
|
216
152
|
}
|
|
217
153
|
}
|
|
218
|
-
|
|
219
|
-
// Save token for next incremental fetch
|
|
220
|
-
savedToken = transaction.token
|
|
154
|
+
return transactions.last?.token ?? lastToken
|
|
221
155
|
}
|
|
222
156
|
```
|
|
223
157
|
|
|
224
|
-
|
|
158
|
+
Persist the returned token and pass it in next time. Sorting a
|
|
159
|
+
`HistoryDescriptor` by `\.timestamp` needs iOS 26.
|
|
225
160
|
|
|
226
|
-
|
|
227
|
-
let cutoffDate = Calendar.current.date(byAdding: .month, value: -3, to: .now)!
|
|
228
|
-
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
|
|
229
|
-
descriptor.predicate = #Predicate<DefaultHistoryTransaction> { transaction in
|
|
230
|
-
transaction.timestamp < cutoffDate
|
|
231
|
-
}
|
|
232
|
-
try modelContext.deleteHistory(descriptor)
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### DefaultHistoryTransaction Properties
|
|
236
|
-
|
|
237
|
-
| Property | Type | Description |
|
|
238
|
-
|----------|------|-------------|
|
|
239
|
-
| `author` | `String?` | The context author that made the change |
|
|
240
|
-
| `changes` | `[HistoryChange]` | Insert, update, delete changes |
|
|
241
|
-
| `storeIdentifier` | `String` | Store that owns the transaction |
|
|
242
|
-
| `timestamp` | `Date` | When the transaction occurred |
|
|
243
|
-
| `token` | `DefaultHistoryToken` | Opaque token for incremental queries |
|
|
244
|
-
| `transactionIdentifier` | ... | Unique transaction ID |
|
|
245
|
-
| `bundleIdentifier` | `String` | Bundle that made the change |
|
|
246
|
-
| `processIdentifier` | `String` | Process that made the change |
|
|
247
|
-
|
|
248
|
-
### Cross-Process Change Detection
|
|
249
|
-
|
|
250
|
-
Use `bundleIdentifier` and `processIdentifier` to differentiate changes from
|
|
251
|
-
widgets, extensions, or the main app.
|
|
161
|
+
Prune old history so it does not grow forever:
|
|
252
162
|
|
|
253
163
|
```swift
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
164
|
+
let cutoff = Date.now.addingTimeInterval(-90 * 24 * 60 * 60)
|
|
165
|
+
var old = HistoryDescriptor<DefaultHistoryTransaction>()
|
|
166
|
+
old.predicate = #Predicate { $0.timestamp < cutoff }
|
|
167
|
+
try context.deleteHistory(old)
|
|
259
168
|
```
|
|
260
169
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
170
|
+
`DefaultHistoryTransaction` carries `author`, `changes`, `storeIdentifier`,
|
|
171
|
+
`timestamp`, `token` (a `DefaultHistoryToken`), `transactionIdentifier`,
|
|
172
|
+
`bundleIdentifier` and `processIdentifier`. Use `author`, `bundleIdentifier`
|
|
173
|
+
and `processIdentifier` to tell app, widget and extension writes apart.
|
|
264
174
|
|
|
265
|
-
|
|
175
|
+
## CloudKit in detail
|
|
266
176
|
|
|
267
|
-
|
|
268
|
-
// Automatic: uses CloudKit entitlement from the app
|
|
269
|
-
let autoConfig = ModelConfiguration(
|
|
270
|
-
cloudKitDatabase: .automatic
|
|
271
|
-
)
|
|
177
|
+
`cloudKitDatabase:` on `ModelConfiguration` takes:
|
|
272
178
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
)
|
|
179
|
+
- `.automatic`: use the container named in the app's entitlements.
|
|
180
|
+
- `.private("iCloud.example.app")`: a specific private database.
|
|
181
|
+
- `.none`: never sync this store.
|
|
277
182
|
|
|
278
|
-
|
|
279
|
-
let localConfig = ModelConfiguration(
|
|
280
|
-
cloudKitDatabase: .none
|
|
281
|
-
)
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Setup Requirements
|
|
183
|
+
Setup:
|
|
285
184
|
|
|
286
|
-
1.
|
|
287
|
-
2.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
185
|
+
1. Add the iCloud capability to the target.
|
|
186
|
+
2. Check CloudKit; this adds the `com.apple.developer.icloud-services`
|
|
187
|
+
entitlement.
|
|
188
|
+
3. Pick or create the container identifier.
|
|
189
|
+
4. Under Background Modes, turn on Remote notifications.
|
|
190
|
+
5. Name the same container in `ModelConfiguration`.
|
|
291
191
|
|
|
292
|
-
|
|
192
|
+
A model that syncs cleanly:
|
|
293
193
|
|
|
294
194
|
```swift
|
|
295
195
|
@Model
|
|
296
|
-
class
|
|
297
|
-
|
|
298
|
-
var title: String = ""
|
|
196
|
+
final class JournalEntry {
|
|
197
|
+
var headline: String = ""
|
|
299
198
|
var body: String?
|
|
199
|
+
@Attribute(.allowsCloudEncryption) var mood: String?
|
|
200
|
+
@Attribute(.externalStorage) var sketch: Data?
|
|
201
|
+
var notebook: Notebook?
|
|
300
202
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// Store large data externally
|
|
305
|
-
@Attribute(.externalStorage) var attachment: Data?
|
|
306
|
-
|
|
307
|
-
// Avoid .unique with CloudKit -- CloudKit does not enforce server-side uniqueness
|
|
308
|
-
// Use @Attribute(.unique) only for local-only stores
|
|
309
|
-
|
|
310
|
-
init(title: String? = nil, body: String? = nil) {
|
|
311
|
-
self.title = title
|
|
203
|
+
init(headline: String, body: String? = nil) {
|
|
204
|
+
self.headline = headline
|
|
312
205
|
self.body = body
|
|
313
206
|
}
|
|
314
207
|
}
|
|
315
208
|
```
|
|
316
209
|
|
|
317
|
-
|
|
210
|
+
`headline` stays a non-optional `String` with a default, and the initializer
|
|
211
|
+
takes a non-optional value for it.
|
|
318
212
|
|
|
319
|
-
-
|
|
320
|
-
|
|
321
|
-
`cloudKitDatabase: .none
|
|
322
|
-
-
|
|
323
|
-
|
|
324
|
-
initializers, or migrations provide valid values.
|
|
325
|
-
- **Delete rules**: `.deny` is unsupported for CloudKit sync; enforce that
|
|
326
|
-
invariant in app logic if needed.
|
|
327
|
-
- **Schema changes**: Initialize and verify the development schema in
|
|
328
|
-
nonproduction builds, promote it before release, and treat production changes
|
|
329
|
-
as additive-only.
|
|
213
|
+
- CloudKit does not enforce uniqueness on the server, so avoid
|
|
214
|
+
`@Attribute(.unique)` and `#Unique` in synced models. A store that needs them
|
|
215
|
+
should use `cloudKitDatabase: .none`.
|
|
216
|
+
- Relationships must be optional. Scalars do not all have to be.
|
|
217
|
+
- `.deny` delete rules are not supported; enforce that invariant in app code.
|
|
330
218
|
|
|
331
|
-
|
|
219
|
+
Keeping part of the data local while the rest syncs:
|
|
332
220
|
|
|
333
221
|
```swift
|
|
334
|
-
let
|
|
335
|
-
|
|
336
|
-
schema: Schema([DraftNote.self]),
|
|
337
|
-
cloudKitDatabase: .none
|
|
338
|
-
)
|
|
222
|
+
let syncedSchema = Schema([JournalEntry.self, Notebook.self])
|
|
223
|
+
let localSchema = Schema([DraftCache.self])
|
|
339
224
|
|
|
340
|
-
let
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
)
|
|
225
|
+
let synced = ModelConfiguration("Synced", schema: syncedSchema,
|
|
226
|
+
cloudKitDatabase: .private("iCloud.example.journal"))
|
|
227
|
+
let local = ModelConfiguration("Local", schema: localSchema,
|
|
228
|
+
cloudKitDatabase: .none)
|
|
345
229
|
|
|
346
230
|
let container = try ModelContainer(
|
|
347
|
-
for: Schema([
|
|
348
|
-
configurations: [
|
|
231
|
+
for: Schema([JournalEntry.self, Notebook.self, DraftCache.self]),
|
|
232
|
+
configurations: [synced, local]
|
|
349
233
|
)
|
|
350
234
|
```
|
|
351
235
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
## Core Data Coexistence and Migration
|
|
355
|
-
|
|
356
|
-
### Three Strategies
|
|
357
|
-
|
|
358
|
-
| Strategy | When to Use |
|
|
359
|
-
|----------|-------------|
|
|
360
|
-
| Pure Core Data | No migration needed; maintain existing stack |
|
|
361
|
-
| Full SwiftData | Greenfield app or complete rewrite |
|
|
362
|
-
| Coexistence | Gradual migration; both stacks share the same store |
|
|
363
|
-
|
|
364
|
-
### Coexistence Setup
|
|
365
|
-
|
|
366
|
-
Both stacks read/write the same SQLite file. Critical requirements:
|
|
367
|
-
|
|
368
|
-
1. **Enable persistent history tracking** on the Core Data side:
|
|
369
|
-
```swift
|
|
370
|
-
let description = NSPersistentStoreDescription()
|
|
371
|
-
description.setOption(
|
|
372
|
-
true as NSNumber,
|
|
373
|
-
forKey: NSPersistentHistoryTrackingKey
|
|
374
|
-
)
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
2. **Match entity names** between Core Data `.xcdatamodeld` and SwiftData
|
|
378
|
-
`@Model` classes.
|
|
379
|
-
|
|
380
|
-
3. **Use different class names** to avoid conflicts:
|
|
381
|
-
```swift
|
|
382
|
-
// Core Data side
|
|
383
|
-
class CDTrip: NSManagedObject { /* ... */ }
|
|
384
|
-
|
|
385
|
-
// SwiftData side
|
|
386
|
-
@Model
|
|
387
|
-
class Trip { /* entity name "Trip" matches Core Data entity */ }
|
|
388
|
-
```
|
|
236
|
+
## Core Data strategies
|
|
389
237
|
|
|
390
|
-
|
|
238
|
+
Three paths for an app that has Core Data today:
|
|
391
239
|
|
|
392
|
-
|
|
240
|
+
| Path | When |
|
|
241
|
+
|---|---|
|
|
242
|
+
| Stay on Core Data | The stack works and nothing needs SwiftData |
|
|
243
|
+
| Move fully to SwiftData | New app, or a planned rewrite |
|
|
244
|
+
| Run both on one store | Gradual move, screen by screen |
|
|
393
245
|
|
|
394
|
-
|
|
395
|
-
|----------|----------|
|
|
396
|
-
| Default | Application Support directory |
|
|
397
|
-
| App group entitlement | Root of app group container |
|
|
398
|
-
| Explicit URL | `ModelConfiguration(url: customURL)` |
|
|
246
|
+
Running both requires:
|
|
399
247
|
|
|
400
|
-
|
|
248
|
+
- Persistent history on the Core Data side:
|
|
249
|
+
set `NSPersistentHistoryTrackingKey` to `true` on the store description
|
|
250
|
+
before loading it.
|
|
251
|
+
- Matching entity names. Keep the Swift class names different so they do not
|
|
252
|
+
collide, and give the Core Data class the same entity name:
|
|
253
|
+
`CDInvoice: NSManagedObject` for the entity `Invoice`, and
|
|
254
|
+
`@Model final class Invoice` on the SwiftData side.
|
|
255
|
+
- Both stacks opening the same store URL.
|
|
401
256
|
|
|
402
|
-
|
|
257
|
+
Where the store lives: by default under Application Support; with an app group,
|
|
258
|
+
at the root of the group container; or anywhere you choose with
|
|
259
|
+
`ModelConfiguration(url:)`.
|
|
403
260
|
|
|
404
|
-
|
|
405
|
-
2. Create `@Model` classes with matching entity/attribute names.
|
|
406
|
-
3. Set up `SchemaMigrationPlan` for future changes.
|
|
407
|
-
4. Enable persistent history tracking on Core Data side.
|
|
408
|
-
5. Point both stacks at the same store file.
|
|
409
|
-
6. Gradually move reads to `@Query` / `FetchDescriptor`.
|
|
410
|
-
7. Move writes to `ModelContext` operations.
|
|
411
|
-
8. Remove Core Data stack when migration is complete.
|
|
261
|
+
Moving over, in order:
|
|
412
262
|
|
|
413
|
-
|
|
263
|
+
1. Write a `VersionedSchema` that matches the current Core Data model.
|
|
264
|
+
2. Write `@Model` classes that match each entity.
|
|
265
|
+
3. Add a `SchemaMigrationPlan`.
|
|
266
|
+
4. Turn on persistent history in Core Data.
|
|
267
|
+
5. Open the shared store file from both stacks.
|
|
268
|
+
6. Move reads to `@Query` and `FetchDescriptor`.
|
|
269
|
+
7. Move writes to `ModelContext`.
|
|
270
|
+
8. Remove the Core Data stack.
|
|
414
271
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
### Batch Enumeration
|
|
418
|
-
|
|
419
|
-
Process large result sets without loading all objects into memory:
|
|
420
|
-
|
|
421
|
-
```swift
|
|
422
|
-
try modelContext.enumerate(
|
|
423
|
-
FetchDescriptor<Trip>(),
|
|
424
|
-
batchSize: 5000,
|
|
425
|
-
allowEscapingMutations: false
|
|
426
|
-
) { trip in
|
|
427
|
-
trip.isProcessed = true
|
|
428
|
-
}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
- `batchSize`: Number of objects loaded per batch (default 5000).
|
|
432
|
-
- `allowEscapingMutations`: Set to `true` only if mutations need to persist
|
|
433
|
-
beyond the enumeration block.
|
|
434
|
-
|
|
435
|
-
### Batch Delete
|
|
436
|
-
|
|
437
|
-
```swift
|
|
438
|
-
try modelContext.delete(
|
|
439
|
-
model: Trip.self,
|
|
440
|
-
where: #Predicate { $0.isArchived == true },
|
|
441
|
-
includeSubclasses: true // iOS 26+ with inheritance
|
|
442
|
-
)
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Fetching Only Identifiers
|
|
446
|
-
|
|
447
|
-
When full objects are not needed (e.g., for counting or cross-actor references):
|
|
448
|
-
|
|
449
|
-
```swift
|
|
450
|
-
let ids = try modelContext.fetchIdentifiers(FetchDescriptor<Trip>())
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Fetch Count
|
|
454
|
-
|
|
455
|
-
```swift
|
|
456
|
-
let count = try modelContext.fetchCount(
|
|
457
|
-
FetchDescriptor<Trip>(predicate: #Predicate { $0.isFavorite == true })
|
|
458
|
-
)
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
### Partial Property Fetch
|
|
462
|
-
|
|
463
|
-
Fetch only specific properties to reduce memory:
|
|
464
|
-
|
|
465
|
-
```swift
|
|
466
|
-
var descriptor = FetchDescriptor<Trip>()
|
|
467
|
-
descriptor.propertiesToFetch = [\.name, \.startDate]
|
|
468
|
-
let trips = try modelContext.fetch(descriptor)
|
|
469
|
-
```
|
|
272
|
+
Details and a migration test: [core-data-coexistence.md](core-data-coexistence.md).
|
|
470
273
|
|
|
471
|
-
|
|
274
|
+
## Batch work and performance
|
|
472
275
|
|
|
473
|
-
|
|
276
|
+
`delete(model:where:includeSubclasses:)` deletes by predicate without loading
|
|
277
|
+
rows. `includeSubclasses` matters once you use model inheritance (iOS 26+).
|
|
474
278
|
|
|
475
279
|
```swift
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
280
|
+
try context.delete(model: Invoice.self,
|
|
281
|
+
where: #Predicate { $0.total == 0 },
|
|
282
|
+
includeSubclasses: true)
|
|
479
283
|
```
|
|
480
284
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
- Use `fetchLimit` and `fetchOffset` for pagination.
|
|
484
|
-
- Use `enumerate` instead of `fetch` for processing large datasets.
|
|
485
|
-
- Use `fetchCount` when only the count is needed.
|
|
486
|
-
- Use `fetchIdentifiers` when only IDs are needed.
|
|
487
|
-
- Use `propertiesToFetch` to limit loaded data.
|
|
488
|
-
- Use `@Attribute(.externalStorage)` for large `Data` payloads such as images
|
|
489
|
-
and blobs.
|
|
490
|
-
- Disable `includePendingChanges` if unsaved data is not needed in results.
|
|
491
|
-
- Call `modelContext.save()` periodically during large imports to flush memory.
|
|
285
|
+
Performance habits:
|
|
492
286
|
|
|
493
|
-
|
|
287
|
+
- Page with `fetchLimit` and `fetchOffset`.
|
|
288
|
+
- Walk large sets with `enumerate`.
|
|
289
|
+
- Count with `fetchCount`; collect IDs with `fetchIdentifiers`.
|
|
290
|
+
- Load only needed columns with `propertiesToFetch`.
|
|
291
|
+
- Keep blobs out of the table with `.externalStorage`.
|
|
292
|
+
- Set `includePendingChanges = false` when unsaved edits do not matter.
|
|
293
|
+
- In long imports, save every few hundred rows.
|
|
494
294
|
|
|
495
|
-
## Complex
|
|
295
|
+
## Complex predicates
|
|
496
296
|
|
|
497
|
-
|
|
297
|
+
These assume an `Invoice` model with `lines`, `client`, `discount`, `memo`,
|
|
298
|
+
`issuedAt`, `total` and `budget`. All outside values are bound before the macro.
|
|
498
299
|
|
|
499
300
|
```swift
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
301
|
+
let minimum = 3
|
|
302
|
+
let cal = Calendar.current
|
|
303
|
+
let week = cal.dateInterval(of: .weekOfYear, for: .now)
|
|
304
|
+
let startOfWeek = week?.start ?? .distantPast
|
|
305
|
+
let endOfWeek = week?.end ?? .distantFuture
|
|
306
|
+
let needle = "tax"
|
|
307
|
+
let searchNotes = true
|
|
308
|
+
let earliest: Date? = nil
|
|
506
309
|
|
|
507
|
-
//
|
|
508
|
-
#Predicate<
|
|
509
|
-
|
|
510
|
-
item.isPacked == true
|
|
511
|
-
}
|
|
310
|
+
// Into a to-many relationship
|
|
311
|
+
let hasBigLine = #Predicate<Invoice> { invoice in
|
|
312
|
+
invoice.lines.contains { $0.quantity > minimum }
|
|
512
313
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
### Optional Chaining
|
|
516
|
-
|
|
517
|
-
```swift
|
|
518
|
-
// Trips with accommodation in a specific city
|
|
519
|
-
#Predicate<Trip> { trip in
|
|
520
|
-
trip.accommodation?.city == "Paris"
|
|
314
|
+
let allPaid = #Predicate<Invoice> { invoice in
|
|
315
|
+
invoice.lines.allSatisfy { $0.isPaid }
|
|
521
316
|
}
|
|
522
317
|
|
|
523
|
-
//
|
|
524
|
-
#Predicate<
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
```
|
|
318
|
+
// Optional chaining and nil coalescing
|
|
319
|
+
let clientInBerlin = #Predicate<Invoice> { $0.client?.city == "Berlin" }
|
|
320
|
+
let lowDiscount = #Predicate<Invoice> { ($0.discount ?? 0) < 10 }
|
|
528
321
|
|
|
529
|
-
|
|
322
|
+
// Prefix and case-insensitive search
|
|
323
|
+
let draftNumber = #Predicate<Invoice> { $0.number.starts(with: "DRAFT-") }
|
|
324
|
+
let mentionsTax = #Predicate<Invoice> { $0.memo.localizedStandardContains(needle) }
|
|
530
325
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
#Predicate<
|
|
534
|
-
trip.destination.localizedStandardContains(searchText)
|
|
535
|
-
}
|
|
326
|
+
// Date window and arithmetic between properties
|
|
327
|
+
let thisWeek = #Predicate<Invoice> { $0.issuedAt >= startOfWeek && $0.issuedAt < endOfWeek }
|
|
328
|
+
let overBudget = #Predicate<Invoice> { $0.total > $0.budget * 1.1 }
|
|
536
329
|
|
|
537
|
-
//
|
|
538
|
-
#Predicate<
|
|
539
|
-
|
|
330
|
+
// Ternary chooses the field to search
|
|
331
|
+
let flexible = #Predicate<Invoice> {
|
|
332
|
+
searchNotes ? $0.memo.localizedStandardContains(needle)
|
|
333
|
+
: $0.number.localizedStandardContains(needle)
|
|
540
334
|
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Date and Numeric Ranges
|
|
544
|
-
|
|
545
|
-
```swift
|
|
546
|
-
let startOfYear = Calendar.current.date(from: DateComponents(year: 2026, month: 1, day: 1))!
|
|
547
|
-
let endOfYear = Calendar.current.date(from: DateComponents(year: 2026, month: 12, day: 31))!
|
|
548
|
-
|
|
549
|
-
#Predicate<Trip> { trip in
|
|
550
|
-
trip.startDate >= startOfYear && trip.startDate <= endOfYear
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Arithmetic
|
|
554
|
-
#Predicate<Trip> { trip in
|
|
555
|
-
trip.budget - trip.spent > 100.0
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
### Ternary Expressions
|
|
560
|
-
|
|
561
|
-
```swift
|
|
562
|
-
#Predicate<Trip> { trip in
|
|
563
|
-
(trip.isFavorite ? trip.name : trip.destination).localizedStandardContains(searchText)
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
335
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
```swift
|
|
572
|
-
func buildPredicate(
|
|
573
|
-
searchText: String,
|
|
574
|
-
onlyFavorites: Bool,
|
|
575
|
-
minDate: Date?
|
|
576
|
-
) -> Predicate<Trip> {
|
|
577
|
-
#Predicate<Trip> { trip in
|
|
578
|
-
(searchText.isEmpty || trip.name.localizedStandardContains(searchText))
|
|
579
|
-
&& (!onlyFavorites || trip.isFavorite == true)
|
|
580
|
-
&& (minDate == nil || trip.startDate >= (minDate ?? .distantPast))
|
|
581
|
-
}
|
|
336
|
+
// Optional parameter that may or may not filter
|
|
337
|
+
let floor = earliest ?? .distantPast
|
|
338
|
+
let sinceMaybe = #Predicate<Invoice> {
|
|
339
|
+
earliest == nil || $0.issuedAt >= floor
|
|
582
340
|
}
|
|
583
341
|
```
|
|
584
342
|
|
|
585
|
-
|
|
343
|
+
With model inheritance (iOS 26+) a predicate can test the concrete type:
|
|
586
344
|
|
|
587
345
|
```swift
|
|
588
|
-
|
|
589
|
-
#Predicate<Trip> { trip in
|
|
590
|
-
trip is BusinessTrip
|
|
591
|
-
}
|
|
346
|
+
let onlyRecurring = #Predicate<Invoice> { $0 is RecurringInvoice }
|
|
592
347
|
```
|
|
593
348
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
## Composite Attributes (iOS 18+)
|
|
349
|
+
## Codable structs as composite attributes
|
|
597
350
|
|
|
598
|
-
Codable
|
|
351
|
+
A `Codable` struct property is stored as a composite attribute
|
|
352
|
+
(`Schema.CompositeAttribute`): its fields become columns in the owning model's
|
|
353
|
+
table rather than a separate table. Optional composites are allowed. Storing
|
|
354
|
+
Codable structs and enums has been supported since SwiftData's first release;
|
|
355
|
+
what varies by OS is how well predicates reach inside them, so test those on
|
|
356
|
+
your oldest deployment target.
|
|
599
357
|
|
|
600
358
|
```swift
|
|
601
|
-
struct
|
|
359
|
+
struct PostalAddress: Codable, Hashable {
|
|
602
360
|
var street: String
|
|
603
361
|
var city: String
|
|
604
|
-
var
|
|
605
|
-
var zip: String
|
|
362
|
+
var postcode: String
|
|
606
363
|
}
|
|
607
364
|
|
|
608
365
|
@Model
|
|
609
|
-
class
|
|
366
|
+
final class Customer {
|
|
610
367
|
var name: String
|
|
611
|
-
var
|
|
612
|
-
var
|
|
368
|
+
var billing: PostalAddress
|
|
369
|
+
var shipping: PostalAddress?
|
|
613
370
|
|
|
614
|
-
init(name: String,
|
|
371
|
+
init(name: String, billing: PostalAddress) {
|
|
615
372
|
self.name = name
|
|
616
|
-
self.
|
|
373
|
+
self.billing = billing
|
|
617
374
|
}
|
|
618
375
|
}
|
|
619
|
-
```
|
|
620
376
|
|
|
621
|
-
|
|
622
|
-
Sub-properties are stored inline in the same table. Query individual fields
|
|
623
|
-
via key-path navigation in `#Predicate`:
|
|
624
|
-
|
|
625
|
-
```swift
|
|
626
|
-
#Predicate<Person> { person in
|
|
627
|
-
person.homeAddress.city == "San Francisco"
|
|
628
|
-
}
|
|
377
|
+
let local = #Predicate<Customer> { $0.billing.city == "Lyon" }
|
|
629
378
|
```
|
|
630
379
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
## Model Inheritance (iOS 26+)
|
|
380
|
+
## Model inheritance
|
|
634
381
|
|
|
635
|
-
|
|
382
|
+
From iOS 26 a `@Model` class can subclass another. The subclass adds stored
|
|
383
|
+
properties and calls `super.init`.
|
|
636
384
|
|
|
637
385
|
```swift
|
|
386
|
+
@available(iOS 26, *)
|
|
638
387
|
@Model
|
|
639
|
-
class
|
|
640
|
-
var
|
|
641
|
-
var
|
|
642
|
-
|
|
643
|
-
var endDate: Date
|
|
644
|
-
|
|
645
|
-
init(name: String, destination: String, startDate: Date, endDate: Date) {
|
|
646
|
-
self.name = name
|
|
647
|
-
self.destination = destination
|
|
648
|
-
self.startDate = startDate
|
|
649
|
-
self.endDate = endDate
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
@Model
|
|
654
|
-
class PersonalTrip: Trip {
|
|
655
|
-
var companion: String?
|
|
388
|
+
class Invoice {
|
|
389
|
+
var number: String
|
|
390
|
+
var total: Decimal
|
|
391
|
+
init(number: String, total: Decimal) { self.number = number; self.total = total }
|
|
656
392
|
}
|
|
657
393
|
|
|
394
|
+
@available(iOS 26, *)
|
|
658
395
|
@Model
|
|
659
|
-
class
|
|
660
|
-
var
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
company: String) {
|
|
665
|
-
self.company = company
|
|
666
|
-
super.init(name: name, destination: destination,
|
|
667
|
-
startDate: startDate, endDate: endDate)
|
|
396
|
+
final class RecurringInvoice: Invoice {
|
|
397
|
+
var intervalMonths: Int
|
|
398
|
+
init(number: String, total: Decimal, intervalMonths: Int) {
|
|
399
|
+
self.intervalMonths = intervalMonths
|
|
400
|
+
super.init(number: number, total: total)
|
|
668
401
|
}
|
|
669
402
|
}
|
|
670
403
|
```
|
|
671
404
|
|
|
672
|
-
|
|
405
|
+
- Fetching `Invoice` returns plain invoices and every subclass.
|
|
406
|
+
- Fetching `RecurringInvoice` returns only that subclass.
|
|
407
|
+
- `ModelContainer(for: Invoice.self)` registers the subclasses too.
|
|
673
408
|
|
|
674
|
-
|
|
675
|
-
// Fetch all trips (includes PersonalTrip and BusinessTrip)
|
|
676
|
-
let allTrips = try modelContext.fetch(FetchDescriptor<Trip>())
|
|
409
|
+
## Several configurations in one container
|
|
677
410
|
|
|
678
|
-
|
|
679
|
-
|
|
411
|
+
Separate stores with different sync settings: see the local plus synced
|
|
412
|
+
example in [CloudKit in detail](#cloudkit-in-detail).
|
|
680
413
|
|
|
681
|
-
|
|
682
|
-
try modelContext.delete(
|
|
683
|
-
model: Trip.self,
|
|
684
|
-
where: #Predicate { $0.destination == "Cancelled" },
|
|
685
|
-
includeSubclasses: true
|
|
686
|
-
)
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
### Container Registration
|
|
690
|
-
|
|
691
|
-
Register the base class; subclasses are included automatically:
|
|
414
|
+
A read-only seed store shipped in the bundle:
|
|
692
415
|
|
|
693
416
|
```swift
|
|
694
|
-
let
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
### Separate Stores for Different Data
|
|
703
|
-
|
|
704
|
-
```swift
|
|
705
|
-
// Local-only data (no sync)
|
|
706
|
-
let localConfig = ModelConfiguration(
|
|
707
|
-
"Local",
|
|
708
|
-
schema: Schema([AppSettings.self, CacheEntry.self]),
|
|
709
|
-
isStoredInMemoryOnly: false,
|
|
710
|
-
cloudKitDatabase: .none
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
// Synced data
|
|
714
|
-
let syncConfig = ModelConfiguration(
|
|
715
|
-
"Synced",
|
|
716
|
-
schema: Schema([UserDocument.self, SharedNote.self]),
|
|
717
|
-
cloudKitDatabase: .private("iCloud.com.example.app")
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
let container = try ModelContainer(
|
|
721
|
-
for: Schema([AppSettings.self, CacheEntry.self, UserDocument.self, SharedNote.self]),
|
|
722
|
-
configurations: [localConfig, syncConfig]
|
|
723
|
-
)
|
|
417
|
+
if let seedURL = Bundle.main.url(forResource: "Species", withExtension: "store") {
|
|
418
|
+
let seed = ModelConfiguration("Seed", schema: Schema([Species.self]),
|
|
419
|
+
url: seedURL, allowsSave: false)
|
|
420
|
+
let user = ModelConfiguration("User", schema: Schema([Sighting.self]))
|
|
421
|
+
let fieldGuide = try ModelContainer(for: Schema([Species.self, Sighting.self]),
|
|
422
|
+
configurations: [seed, user])
|
|
423
|
+
}
|
|
724
424
|
```
|
|
725
425
|
|
|
726
|
-
|
|
426
|
+
Sharing with widgets and extensions:
|
|
727
427
|
|
|
728
428
|
```swift
|
|
729
|
-
let
|
|
730
|
-
let readOnlyConfig = ModelConfiguration(
|
|
731
|
-
"SeedData",
|
|
732
|
-
schema: Schema([ReferenceItem.self]),
|
|
733
|
-
url: bundledURL,
|
|
734
|
-
allowsSave: false
|
|
735
|
-
)
|
|
429
|
+
let shared = ModelConfiguration(groupContainer: .identifier("group.example.journal"))
|
|
736
430
|
```
|
|
737
431
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
```swift
|
|
741
|
-
let sharedConfig = ModelConfiguration(
|
|
742
|
-
groupContainer: .identifier("group.com.example.myapp")
|
|
743
|
-
)
|
|
744
|
-
let container = try ModelContainer(for: Trip.self, configurations: sharedConfig)
|
|
745
|
-
```
|
|
432
|
+
## Undo and redo
|
|
746
433
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
## Undo/Redo Support
|
|
750
|
-
|
|
751
|
-
### Setup
|
|
752
|
-
|
|
753
|
-
```swift
|
|
754
|
-
let context = ModelContext(container)
|
|
755
|
-
context.undoManager = UndoManager()
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
### SwiftUI Integration
|
|
434
|
+
Give the context an `UndoManager`, for example once at launch:
|
|
759
435
|
|
|
760
436
|
```swift
|
|
761
437
|
@main
|
|
762
|
-
struct
|
|
438
|
+
struct LedgerApp: App {
|
|
763
439
|
let container: ModelContainer
|
|
764
440
|
|
|
765
441
|
init() {
|
|
766
442
|
do {
|
|
767
|
-
container = try ModelContainer(for:
|
|
768
|
-
container.mainContext.undoManager = UndoManager()
|
|
443
|
+
container = try ModelContainer(for: Invoice.self)
|
|
769
444
|
} catch {
|
|
770
|
-
fatalError("
|
|
445
|
+
fatalError("Store failed to open: \(error)")
|
|
771
446
|
}
|
|
447
|
+
container.mainContext.undoManager = UndoManager()
|
|
772
448
|
}
|
|
773
449
|
|
|
774
450
|
var body: some Scene {
|
|
775
|
-
WindowGroup {
|
|
776
|
-
ContentView()
|
|
777
|
-
}
|
|
778
|
-
.modelContainer(container)
|
|
451
|
+
WindowGroup { InvoiceList() }.modelContainer(container)
|
|
779
452
|
}
|
|
780
453
|
}
|
|
781
454
|
```
|
|
782
455
|
|
|
783
|
-
|
|
456
|
+
Or adopt the window's undo manager from a view:
|
|
784
457
|
|
|
785
458
|
```swift
|
|
786
|
-
struct
|
|
787
|
-
@Environment(\.modelContext) private var
|
|
459
|
+
struct UndoControls: View {
|
|
460
|
+
@Environment(\.modelContext) private var context
|
|
788
461
|
@Environment(\.undoManager) private var undoManager
|
|
789
462
|
|
|
790
463
|
var body: some View {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
Button("Undo") {
|
|
797
|
-
modelContext.undoManager?.undo()
|
|
798
|
-
}
|
|
799
|
-
.disabled(!(modelContext.undoManager?.canUndo ?? false))
|
|
800
|
-
|
|
801
|
-
Button("Redo") {
|
|
802
|
-
modelContext.undoManager?.redo()
|
|
803
|
-
}
|
|
804
|
-
.disabled(!(modelContext.undoManager?.canRedo ?? false))
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
.onAppear {
|
|
808
|
-
modelContext.undoManager = undoManager
|
|
464
|
+
HStack {
|
|
465
|
+
Button("Undo") { context.undoManager?.undo() }
|
|
466
|
+
.disabled(!(context.undoManager?.canUndo ?? false))
|
|
467
|
+
Button("Redo") { context.undoManager?.redo() }
|
|
468
|
+
.disabled(!(context.undoManager?.canRedo ?? false))
|
|
809
469
|
}
|
|
470
|
+
.onAppear { context.undoManager = undoManager }
|
|
810
471
|
}
|
|
811
472
|
}
|
|
812
473
|
```
|
|
813
474
|
|
|
814
|
-
|
|
475
|
+
After an insert that must be undoable as its own step, call
|
|
476
|
+
`context.processPendingChanges()` so the undo manager records it.
|
|
815
477
|
|
|
816
|
-
|
|
817
|
-
modelContext.insert(trip)
|
|
818
|
-
modelContext.processPendingChanges()
|
|
819
|
-
// Now undo is available for the insertion
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
---
|
|
823
|
-
|
|
824
|
-
## Preview Patterns with In-Memory Stores
|
|
478
|
+
## Previews with in-memory stores
|
|
825
479
|
|
|
826
|
-
|
|
480
|
+
One shared container, seeded once:
|
|
827
481
|
|
|
828
482
|
```swift
|
|
829
483
|
@MainActor
|
|
830
|
-
let
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
container.mainContext.insert(trip)
|
|
484
|
+
let sampleLedger: ModelContainer = {
|
|
485
|
+
do {
|
|
486
|
+
let container = try ModelContainer(
|
|
487
|
+
for: Customer.self, Invoice.self,
|
|
488
|
+
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
|
489
|
+
)
|
|
490
|
+
let customer = Customer(name: "Ada",
|
|
491
|
+
billing: PostalAddress(street: "1 Main", city: "Lyon", postcode: "69001"))
|
|
492
|
+
container.mainContext.insert(customer)
|
|
493
|
+
container.mainContext.insert(Invoice(number: "INV-1", total: 120))
|
|
494
|
+
return container
|
|
495
|
+
} catch {
|
|
496
|
+
fatalError("Preview store failed: \(error)")
|
|
844
497
|
}
|
|
845
|
-
|
|
846
|
-
return container
|
|
847
498
|
}()
|
|
848
499
|
|
|
849
|
-
#Preview {
|
|
850
|
-
TripListView()
|
|
851
|
-
.modelContainer(previewContainer)
|
|
852
|
-
}
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
### Preview with Relationships
|
|
856
|
-
|
|
857
|
-
```swift
|
|
858
|
-
#Preview {
|
|
859
|
-
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
|
860
|
-
let container = try! ModelContainer(
|
|
861
|
-
for: Trip.self, LivingAccommodation.self,
|
|
862
|
-
configurations: config
|
|
863
|
-
)
|
|
864
|
-
|
|
865
|
-
let trip = Trip(name: "Beach Trip", destination: "Malibu",
|
|
866
|
-
startDate: .now, endDate: .now.addingTimeInterval(86400 * 3))
|
|
867
|
-
let hotel = LivingAccommodation(name: "Beach Resort")
|
|
868
|
-
trip.accommodation = hotel
|
|
869
|
-
|
|
870
|
-
container.mainContext.insert(trip)
|
|
871
|
-
|
|
872
|
-
return TripDetailView(trip: trip)
|
|
873
|
-
.modelContainer(container)
|
|
874
|
-
}
|
|
500
|
+
#Preview { InvoiceList().modelContainer(sampleLedger) }
|
|
875
501
|
```
|
|
876
502
|
|
|
877
|
-
|
|
503
|
+
With relationships, register every related type, connect the objects, then
|
|
504
|
+
insert the parent; inserting the parent inserts the children with it.
|
|
878
505
|
|
|
879
|
-
|
|
506
|
+
From iOS 18, a `PreviewModifier` builds the container once and shares it across
|
|
507
|
+
previews:
|
|
880
508
|
|
|
881
509
|
```swift
|
|
882
|
-
struct
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
510
|
+
struct LedgerSampleData: PreviewModifier {
|
|
511
|
+
typealias Context = ModelContainer
|
|
512
|
+
|
|
513
|
+
static func makeSharedContext() async throws -> Context {
|
|
514
|
+
let container = try ModelContainer(
|
|
515
|
+
for: Invoice.self,
|
|
516
|
+
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
|
517
|
+
)
|
|
518
|
+
container.mainContext.insert(Invoice(number: "INV-7", total: 42))
|
|
887
519
|
return container
|
|
888
520
|
}
|
|
889
521
|
|
|
890
|
-
func body(content: Content, context:
|
|
522
|
+
func body(content: Content, context: Context) -> some View {
|
|
891
523
|
content.modelContainer(context)
|
|
892
524
|
}
|
|
893
525
|
}
|
|
894
526
|
|
|
895
|
-
extension PreviewTrait
|
|
896
|
-
static var
|
|
527
|
+
extension PreviewTrait<Preview.ViewTraits> {
|
|
528
|
+
@MainActor static var ledgerSample: Self = .modifier(LedgerSampleData())
|
|
897
529
|
}
|
|
898
530
|
|
|
899
|
-
#Preview(traits: .
|
|
900
|
-
TripListView()
|
|
901
|
-
}
|
|
531
|
+
#Preview(traits: .ledgerSample) { InvoiceList() }
|
|
902
532
|
```
|
|
903
533
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
## Notification Observation
|
|
534
|
+
## Save notifications
|
|
907
535
|
|
|
908
|
-
|
|
536
|
+
Observe `ModelContext.didSave` for a specific context:
|
|
909
537
|
|
|
910
538
|
```swift
|
|
911
|
-
NotificationCenter.default.
|
|
912
|
-
.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if let updatedIDs = notification.userInfo?[
|
|
920
|
-
ModelContext.NotificationKey.updatedIdentifiers
|
|
921
|
-
] as? Set<PersistentIdentifier> {
|
|
922
|
-
// Handle updates
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if let deletedIDs = notification.userInfo?[
|
|
926
|
-
ModelContext.NotificationKey.deletedIdentifiers
|
|
927
|
-
] as? Set<PersistentIdentifier> {
|
|
928
|
-
// Handle deletions
|
|
929
|
-
}
|
|
930
|
-
}
|
|
539
|
+
let token = NotificationCenter.default.addObserver(
|
|
540
|
+
forName: ModelContext.didSave, object: context, queue: .main
|
|
541
|
+
) { note in
|
|
542
|
+
let inserted = note.userInfo?[ModelContext.NotificationKey.insertedIdentifiers.rawValue]
|
|
543
|
+
as? Set<PersistentIdentifier> ?? []
|
|
544
|
+
refreshBadges(for: inserted)
|
|
545
|
+
}
|
|
931
546
|
```
|
|
932
547
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
| `.insertedIdentifiers` | IDs of newly inserted models |
|
|
938
|
-
| `.updatedIdentifiers` | IDs of updated models |
|
|
939
|
-
| `.deletedIdentifiers` | IDs of deleted models |
|
|
940
|
-
| `.invalidatedAllIdentifiers` | All data invalidated (e.g., store reset) |
|
|
941
|
-
| `.queryGeneration` | Query generation token |
|
|
942
|
-
|
|
943
|
-
---
|
|
548
|
+
Keys under `ModelContext.NotificationKey`: `.insertedIdentifiers`,
|
|
549
|
+
`.updatedIdentifiers` and `.deletedIdentifiers` (each a
|
|
550
|
+
`Set<PersistentIdentifier>`), plus `.invalidatedAllIdentifiers` and
|
|
551
|
+
`.queryGeneration`.
|
|
944
552
|
|
|
945
|
-
##
|
|
553
|
+
## Errors
|
|
946
554
|
|
|
947
|
-
|
|
555
|
+
Catch `SwiftDataError` and compare against its static values:
|
|
948
556
|
|
|
949
557
|
```swift
|
|
950
558
|
do {
|
|
951
|
-
|
|
559
|
+
try context.save()
|
|
952
560
|
} catch let error as SwiftDataError {
|
|
953
561
|
switch error {
|
|
954
|
-
case
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
// Container could not load the store
|
|
562
|
+
case .modelValidationFailure:
|
|
563
|
+
// for example a uniqueness conflict
|
|
564
|
+
break
|
|
565
|
+
case .unsupportedPredicate, .unsupportedSortDescriptor:
|
|
566
|
+
break
|
|
567
|
+
case .loadIssueModelContainer:
|
|
568
|
+
break
|
|
962
569
|
default:
|
|
963
|
-
|
|
570
|
+
break
|
|
964
571
|
}
|
|
965
572
|
} catch {
|
|
966
|
-
//
|
|
573
|
+
// not a SwiftDataError; without this clause the do-catch is not exhaustive
|
|
967
574
|
}
|
|
968
575
|
```
|
|
969
576
|
|
|
970
|
-
|
|
577
|
+
Grouped by where they come from:
|
|
971
578
|
|
|
972
|
-
|
|
|
973
|
-
|
|
579
|
+
| Area | Errors |
|
|
580
|
+
|---|---|
|
|
974
581
|
| Fetch | `.unsupportedPredicate`, `.unsupportedSortDescriptor`, `.unsupportedKeyPath`, `.includePendingChangesWithBatchSize` |
|
|
975
582
|
| Configuration | `.duplicateConfiguration`, `.configurationFileNameContainsInvalidCharacters`, `.configurationSchemaNotFoundInContainerSchema` |
|
|
976
583
|
| Container | `.loadIssueModelContainer` |
|