@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,332 +1,237 @@
|
|
|
1
|
-
# SwiftData
|
|
1
|
+
# SwiftData queries
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
sort descriptors, sectioned queries, dynamic query switching, background fetch
|
|
5
|
-
patterns, and aggregate queries.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Contents
|
|
10
|
-
|
|
11
|
-
- [`@Query Initializer Variants`](#query-initializer-variants)
|
|
12
|
-
- [FetchDescriptor Deep Dive](#fetchdescriptor-deep-dive)
|
|
13
|
-
- [Complex Sort Descriptors](#complex-sort-descriptors)
|
|
14
|
-
- [Sectioned Queries Pattern](#sectioned-queries-pattern)
|
|
15
|
-
- [Dynamic Query Switching](#dynamic-query-switching)
|
|
16
|
-
- [Background Fetch Patterns with `@ModelActor`](#background-fetch-patterns-with-modelactor)
|
|
17
|
-
- [Aggregate Queries](#aggregate-queries)
|
|
18
|
-
- [Enumerate for Large Datasets](#enumerate-for-large-datasets)
|
|
19
|
-
|
|
20
|
-
## `@Query` Initializer Variants
|
|
21
|
-
|
|
22
|
-
`@Query` is a SwiftUI property wrapper (`DynamicProperty`) that automatically
|
|
23
|
-
fetches and observes persistent model data. All variants are `@MainActor`.
|
|
24
|
-
|
|
25
|
-
### Basic (No Filter, No Sort)
|
|
3
|
+
Examples use this model:
|
|
26
4
|
|
|
27
5
|
```swift
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
6
|
+
import SwiftData
|
|
7
|
+
import SwiftUI
|
|
8
|
+
|
|
9
|
+
@Model
|
|
10
|
+
final class Workout {
|
|
11
|
+
var title: String
|
|
12
|
+
var startedAt: Date
|
|
13
|
+
var finishedAt: Date?
|
|
14
|
+
var minutes: Int
|
|
15
|
+
var calories: Double
|
|
16
|
+
var isFavorite: Bool = false
|
|
17
|
+
|
|
18
|
+
init(title: String, startedAt: Date, minutes: Int, calories: Double) {
|
|
19
|
+
self.title = title
|
|
20
|
+
self.startedAt = startedAt
|
|
21
|
+
self.minutes = minutes
|
|
22
|
+
self.calories = calories
|
|
23
|
+
}
|
|
24
|
+
}
|
|
36
25
|
```
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```swift
|
|
41
|
-
@Query(
|
|
42
|
-
filter: #Predicate<Trip> { $0.isFavorite == true },
|
|
43
|
-
sort: [SortDescriptor(\.startDate, order: .reverse)]
|
|
44
|
-
)
|
|
45
|
-
private var favoriteTrips: [Trip]
|
|
46
|
-
|
|
47
|
-
// With animation
|
|
48
|
-
@Query(
|
|
49
|
-
filter: #Predicate<Trip> { $0.isFavorite == true },
|
|
50
|
-
sort: [SortDescriptor(\.startDate, order: .reverse)],
|
|
51
|
-
animation: .default
|
|
52
|
-
)
|
|
53
|
-
private var favoriteTrips: [Trip]
|
|
54
|
-
```
|
|
27
|
+
## Contents
|
|
55
28
|
|
|
56
|
-
|
|
29
|
+
- [@Query initializers](#query-initializers)
|
|
30
|
+
- [FetchDescriptor in depth](#fetchdescriptor-in-depth)
|
|
31
|
+
- [Sorting](#sorting)
|
|
32
|
+
- [Grouping results into sections](#grouping-results-into-sections)
|
|
33
|
+
- [Changing the query at runtime](#changing-the-query-at-runtime)
|
|
34
|
+
- [Fetching on a background actor](#fetching-on-a-background-actor)
|
|
35
|
+
- [Aggregates](#aggregates)
|
|
36
|
+
- [enumerate for large sets](#enumerate-for-large-sets)
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
@Query(
|
|
60
|
-
filter: #Predicate<Trip> { $0.destination != "" },
|
|
61
|
-
sort: \.startDate,
|
|
62
|
-
order: .forward
|
|
63
|
-
)
|
|
64
|
-
private var upcomingTrips: [Trip]
|
|
65
|
-
|
|
66
|
-
// With optional key path sort
|
|
67
|
-
@Query(
|
|
68
|
-
sort: \.endDate, // KeyPath<Trip, Date?> -- optional sort key
|
|
69
|
-
order: .reverse
|
|
70
|
-
)
|
|
71
|
-
private var tripsByEndDate: [Trip]
|
|
72
|
-
```
|
|
38
|
+
## @Query initializers
|
|
73
39
|
|
|
74
|
-
|
|
40
|
+
`@Query` is a SwiftUI `DynamicProperty`, and every initializer is `@MainActor`.
|
|
75
41
|
|
|
76
42
|
```swift
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@Query(
|
|
43
|
+
struct QueryShapes: View {
|
|
44
|
+
// Everything, default order
|
|
45
|
+
@Query private var all: [Workout]
|
|
46
|
+
|
|
47
|
+
// Animate changes
|
|
48
|
+
@Query(animation: .snappy) private var animated: [Workout]
|
|
49
|
+
@Query(transaction: Transaction(animation: .easeOut)) private var viaTransaction: [Workout]
|
|
50
|
+
|
|
51
|
+
// Filter plus an array of sort descriptors
|
|
52
|
+
@Query(filter: #Predicate<Workout> { $0.minutes >= 30 },
|
|
53
|
+
sort: [SortDescriptor(\Workout.startedAt, order: .reverse)],
|
|
54
|
+
animation: .default)
|
|
55
|
+
private var long: [Workout]
|
|
56
|
+
|
|
57
|
+
// Filter plus a single key path; optional keys are allowed
|
|
58
|
+
@Query(filter: #Predicate<Workout> { $0.isFavorite },
|
|
59
|
+
sort: \Workout.finishedAt, order: .forward)
|
|
60
|
+
private var favorites: [Workout]
|
|
61
|
+
|
|
62
|
+
// A prepared descriptor
|
|
63
|
+
@Query(QueryShapes.lastTen) private var recent: [Workout]
|
|
64
|
+
@Query(QueryShapes.lastTen, animation: .default) private var recentAnimated: [Workout]
|
|
65
|
+
|
|
66
|
+
static var lastTen: FetchDescriptor<Workout> {
|
|
67
|
+
var d = FetchDescriptor<Workout>(sortBy: [SortDescriptor(\.startedAt, order: .reverse)])
|
|
68
|
+
d.fetchLimit = 10
|
|
69
|
+
return d
|
|
70
|
+
}
|
|
87
71
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
private var recentTrips: [Trip]
|
|
72
|
+
var body: some View { Text("\(all.count)") }
|
|
73
|
+
}
|
|
91
74
|
```
|
|
92
75
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| `wrappedValue` | `Result` (typically `[Element]`) | Most recent fetched results |
|
|
98
|
-
| `modelContext` | `ModelContext` | The context used for fetching |
|
|
99
|
-
| `fetchError` | `(any Error)?` | Error from most recent fetch, if any |
|
|
100
|
-
|
|
101
|
-
Access `fetchError` to detect query failures:
|
|
76
|
+
A query exposes its results as `wrappedValue`, the `modelContext` it reads
|
|
77
|
+
from, and `fetchError: (any Error)?` for a fetch that failed. The wrapper has
|
|
78
|
+
no projected value, so reach it through the underscored `_workouts`. Surface
|
|
79
|
+
that error instead of showing an empty list:
|
|
102
80
|
|
|
103
81
|
```swift
|
|
104
|
-
struct
|
|
105
|
-
@Query private var
|
|
82
|
+
struct WorkoutHistory: View {
|
|
83
|
+
@Query(sort: \Workout.startedAt, order: .reverse) private var workouts: [Workout]
|
|
106
84
|
|
|
107
85
|
var body: some View {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
List(trips) { trip in
|
|
115
|
-
Text(trip.name)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
86
|
+
if let error = _workouts.fetchError {
|
|
87
|
+
ContentUnavailableView("Could not load workouts",
|
|
88
|
+
systemImage: "exclamationmark.triangle",
|
|
89
|
+
description: Text(error.localizedDescription))
|
|
90
|
+
} else {
|
|
91
|
+
List(workouts) { Text($0.title) }
|
|
118
92
|
}
|
|
119
93
|
}
|
|
120
94
|
}
|
|
121
95
|
```
|
|
122
96
|
|
|
123
|
-
|
|
97
|
+
## FetchDescriptor in depth
|
|
124
98
|
|
|
125
|
-
|
|
99
|
+
| Property | Type | Default |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `predicate` | `Predicate<T>?` | `nil` |
|
|
102
|
+
| `sortBy` | `[SortDescriptor<T>]` | `[]` |
|
|
103
|
+
| `fetchLimit` | `Int?` | `nil` |
|
|
104
|
+
| `fetchOffset` | `Int?` | `nil` |
|
|
105
|
+
| `includePendingChanges` | `Bool` | `true` |
|
|
106
|
+
| `propertiesToFetch` | `[PartialKeyPath<T>]` | every attribute |
|
|
107
|
+
| `relationshipKeyPathsForPrefetching` | `[PartialKeyPath<T>]` | `[]` |
|
|
126
108
|
|
|
127
|
-
|
|
109
|
+
Paging needs a stable sort, otherwise rows move between pages:
|
|
128
110
|
|
|
129
111
|
```swift
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
| `fetchOffset` | `Int?` | `nil` | Skip first N results |
|
|
139
|
-
| `includePendingChanges` | `Bool` | `true` | Include unsaved in-memory changes |
|
|
140
|
-
| `propertiesToFetch` | `[PartialKeyPath<T>]` | all | Specific properties to load |
|
|
141
|
-
| `relationshipKeyPathsForPrefetching` | `[PartialKeyPath<T>]` | `[]` | Related models to eagerly load |
|
|
142
|
-
|
|
143
|
-
### fetchLimit and fetchOffset (Pagination)
|
|
144
|
-
|
|
145
|
-
```swift
|
|
146
|
-
func fetchPage(page: Int, pageSize: Int) throws -> [Trip] {
|
|
147
|
-
var descriptor = FetchDescriptor<Trip>(
|
|
148
|
-
sortBy: [SortDescriptor(\.startDate)]
|
|
149
|
-
)
|
|
150
|
-
descriptor.fetchLimit = pageSize
|
|
151
|
-
descriptor.fetchOffset = page * pageSize
|
|
152
|
-
return try modelContext.fetch(descriptor)
|
|
112
|
+
func page(_ index: Int, size: Int, in context: ModelContext) throws -> [Workout] {
|
|
113
|
+
var d = FetchDescriptor<Workout>(sortBy: [
|
|
114
|
+
SortDescriptor(\.startedAt, order: .reverse),
|
|
115
|
+
SortDescriptor(\.title)
|
|
116
|
+
])
|
|
117
|
+
d.fetchLimit = size
|
|
118
|
+
d.fetchOffset = index * size
|
|
119
|
+
return try context.fetch(d)
|
|
153
120
|
}
|
|
154
121
|
```
|
|
155
122
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
the
|
|
123
|
+
- `includePendingChanges` is `true` by default: results include inserts and
|
|
124
|
+
edits in this context that are not saved yet. Set it to `false` to read only
|
|
125
|
+
what is on disk.
|
|
126
|
+
- Batched fetches need `includePendingChanges = false`. Calling
|
|
127
|
+
`fetch(_:batchSize:)` while it is still `true` (the default) throws
|
|
128
|
+
`SwiftDataError.includePendingChangesWithBatchSize`.
|
|
129
|
+
- `propertiesToFetch` loads only the listed attributes; any other attribute is
|
|
130
|
+
loaded when first touched.
|
|
131
|
+
- `relationshipKeyPathsForPrefetching` loads the named relationships up front,
|
|
132
|
+
which avoids one extra fetch per row.
|
|
133
|
+
|
|
134
|
+
Fetch calls on `ModelContext`:
|
|
135
|
+
|
|
136
|
+
| Call | Returns |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `fetch(_:)` | `[T]` |
|
|
139
|
+
| `fetch(_:batchSize:)` | `FetchResultsCollection<T>` |
|
|
140
|
+
| `fetchCount(_:)` | `Int` |
|
|
141
|
+
| `fetchIdentifiers(_:)` | `[PersistentIdentifier]` |
|
|
142
|
+
| `fetchIdentifiers(_:batchSize:)` | `FetchResultsCollection<PersistentIdentifier>` |
|
|
143
|
+
| `enumerate(_:batchSize:allowEscapingMutations:block:)` | `Void` |
|
|
144
|
+
|
|
145
|
+
A batched collection is walked lazily, keeping about one batch in memory at a
|
|
146
|
+
time. Treat its elements as read-only snapshots.
|
|
161
147
|
|
|
162
148
|
```swift
|
|
163
|
-
var
|
|
164
|
-
|
|
149
|
+
var d = FetchDescriptor<Workout>()
|
|
150
|
+
d.includePendingChanges = false
|
|
151
|
+
let batched = try context.fetch(d, batchSize: 200)
|
|
152
|
+
let totalMinutes = batched.reduce(0) { $0 + $1.minutes }
|
|
165
153
|
```
|
|
166
154
|
|
|
167
|
-
|
|
168
|
-
(`fetch(_:batchSize:)`). This will throw
|
|
169
|
-
`SwiftDataError.includePendingChangesWithBatchSize`.
|
|
170
|
-
|
|
171
|
-
### propertiesToFetch (Partial Loading)
|
|
155
|
+
## Sorting
|
|
172
156
|
|
|
173
|
-
|
|
157
|
+
`SortDescriptor(\.key)` sorts ascending; pass `order: .reverse` for
|
|
158
|
+
descending. Several descriptors sort by the first, then break ties with the
|
|
159
|
+
next:
|
|
174
160
|
|
|
175
161
|
```swift
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### relationshipKeyPathsForPrefetching
|
|
183
|
-
|
|
184
|
-
Eagerly load related objects to avoid N+1 query patterns:
|
|
185
|
-
|
|
186
|
-
```swift
|
|
187
|
-
var descriptor = FetchDescriptor<Trip>()
|
|
188
|
-
descriptor.relationshipKeyPathsForPrefetching = [
|
|
189
|
-
\.accommodation,
|
|
190
|
-
\.tags
|
|
162
|
+
let ordering = [
|
|
163
|
+
SortDescriptor(\Workout.calories, order: .reverse),
|
|
164
|
+
SortDescriptor(\Workout.startedAt, order: .reverse),
|
|
165
|
+
SortDescriptor(\Workout.title)
|
|
191
166
|
]
|
|
192
|
-
let trips = try modelContext.fetch(descriptor)
|
|
193
|
-
// Accessing trip.accommodation does not trigger a separate fetch
|
|
194
167
|
```
|
|
195
168
|
|
|
196
|
-
|
|
169
|
+
A `Bool` property cannot be a sort key: `SortDescriptor` needs a `Comparable`
|
|
170
|
+
value and `Bool` is not one. Sorting on an optional key works, but do not rely on where `nil` rows land;
|
|
171
|
+
with the SQLite store, `nil` sorts before other values in ascending order.
|
|
172
|
+
Add a tie-breaking descriptor or filter out `nil` when placement matters.
|
|
197
173
|
|
|
198
|
-
|
|
199
|
-
|--------|---------|----------|
|
|
200
|
-
| `fetch(_:)` | `[T]` | Standard fetch, all results in memory |
|
|
201
|
-
| `fetch(_:batchSize:)` | `FetchResultsCollection<T>` | Lazy batched loading |
|
|
202
|
-
| `fetchCount(_:)` | `Int` | Count only, no objects loaded |
|
|
203
|
-
| `fetchIdentifiers(_:)` | `[PersistentIdentifier]` | IDs only, lightweight |
|
|
204
|
-
| `fetchIdentifiers(_:batchSize:)` | `FetchResultsCollection<PersistentIdentifier>` | Batched ID loading |
|
|
205
|
-
| `enumerate(_:batchSize:...)` | `Void` | Process large sets in batches |
|
|
206
|
-
|
|
207
|
-
### FetchResultsCollection (Batched Fetch)
|
|
174
|
+
Let the user pick the sort by rebuilding the query in a child view:
|
|
208
175
|
|
|
209
176
|
```swift
|
|
210
|
-
|
|
211
|
-
FetchDescriptor<Trip>(sortBy: [SortDescriptor(\.name)]),
|
|
212
|
-
batchSize: 100
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
// Iterate lazily -- only 100 objects in memory at a time
|
|
216
|
-
for trip in results {
|
|
217
|
-
print(trip.name)
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Requirements for batched fetch:
|
|
222
|
-
- `includePendingChanges` must be `false` (or will throw).
|
|
223
|
-
- Results are read-only snapshots.
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## Complex Sort Descriptors
|
|
228
|
-
|
|
229
|
-
### Single Sort
|
|
230
|
-
|
|
231
|
-
```swift
|
|
232
|
-
SortDescriptor(\.name, order: .forward) // A-Z
|
|
233
|
-
SortDescriptor(\.name, order: .reverse) // Z-A
|
|
234
|
-
SortDescriptor(\.startDate) // Ascending (default)
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Multi-Level Sort
|
|
238
|
-
|
|
239
|
-
```swift
|
|
240
|
-
let descriptor = FetchDescriptor<Trip>(
|
|
241
|
-
sortBy: [
|
|
242
|
-
SortDescriptor(\.isFavorite, order: .reverse), // Favorites first
|
|
243
|
-
SortDescriptor(\.startDate, order: .forward), // Then by date
|
|
244
|
-
SortDescriptor(\.name, order: .forward) // Then alphabetical
|
|
245
|
-
]
|
|
246
|
-
)
|
|
247
|
-
```
|
|
177
|
+
enum WorkoutSortKey: String, CaseIterable { case date, duration, title }
|
|
248
178
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
```swift
|
|
254
|
-
@Query(sort: \.endDate, order: .reverse) private var trips: [Trip]
|
|
255
|
-
// endDate is Date? -- trips without endDate appear last
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### Dynamic Sort Switching
|
|
259
|
-
|
|
260
|
-
```swift
|
|
261
|
-
struct TripListView: View {
|
|
262
|
-
@State private var sortOrder: SortOrder = .forward
|
|
263
|
-
@State private var sortKey: TripSortKey = .name
|
|
179
|
+
struct WorkoutBrowser: View {
|
|
180
|
+
@State private var key: WorkoutSortKey = .date
|
|
181
|
+
@State private var order: SortOrder = .reverse
|
|
264
182
|
|
|
265
183
|
var body: some View {
|
|
266
|
-
|
|
184
|
+
SortedWorkouts(key: key, order: order)
|
|
267
185
|
.toolbar {
|
|
268
|
-
Picker("Sort", selection: $
|
|
269
|
-
|
|
270
|
-
Text("Date").tag(TripSortKey.date)
|
|
271
|
-
Text("Destination").tag(TripSortKey.destination)
|
|
186
|
+
Picker("Sort", selection: $key) {
|
|
187
|
+
ForEach(WorkoutSortKey.allCases, id: \.self) { Text($0.rawValue) }
|
|
272
188
|
}
|
|
273
189
|
}
|
|
274
190
|
}
|
|
275
191
|
}
|
|
276
192
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
struct SortedTripList: View {
|
|
282
|
-
@Query private var trips: [Trip]
|
|
193
|
+
struct SortedWorkouts: View {
|
|
194
|
+
@Query private var workouts: [Workout]
|
|
283
195
|
|
|
284
|
-
init(
|
|
285
|
-
let
|
|
286
|
-
|
|
287
|
-
case .date: SortDescriptor(\.
|
|
288
|
-
case .
|
|
196
|
+
init(key: WorkoutSortKey, order: SortOrder) {
|
|
197
|
+
let descriptor: SortDescriptor<Workout>
|
|
198
|
+
switch key {
|
|
199
|
+
case .date: descriptor = SortDescriptor(\.startedAt, order: order)
|
|
200
|
+
case .duration: descriptor = SortDescriptor(\.minutes, order: order)
|
|
201
|
+
case .title: descriptor = SortDescriptor(\.title, order: order)
|
|
289
202
|
}
|
|
290
|
-
|
|
203
|
+
_workouts = Query(sort: [descriptor])
|
|
291
204
|
}
|
|
292
205
|
|
|
293
|
-
var body: some View {
|
|
294
|
-
List(trips) { trip in
|
|
295
|
-
TripRow(trip: trip)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
206
|
+
var body: some View { List(workouts) { Text($0.title) } }
|
|
298
207
|
}
|
|
299
208
|
```
|
|
300
209
|
|
|
301
|
-
|
|
210
|
+
## Grouping results into sections
|
|
302
211
|
|
|
303
|
-
|
|
212
|
+
There is no sectioned fetch comparable to `NSFetchedResultsController`
|
|
213
|
+
sections. Group the query results yourself.
|
|
304
214
|
|
|
305
|
-
|
|
306
|
-
`NSFetchedResultsController`. Build sectioned views manually:
|
|
307
|
-
|
|
308
|
-
### Using Dictionary Grouping
|
|
215
|
+
By a derived key:
|
|
309
216
|
|
|
310
217
|
```swift
|
|
311
|
-
struct
|
|
312
|
-
@Query(sort:
|
|
313
|
-
|
|
314
|
-
private var
|
|
315
|
-
let
|
|
316
|
-
|
|
317
|
-
let grouped = Dictionary(grouping: trips) { trip in
|
|
318
|
-
formatter.string(from: trip.startDate)
|
|
218
|
+
struct WorkoutsByWeek: View {
|
|
219
|
+
@Query(sort: \Workout.startedAt, order: .reverse) private var workouts: [Workout]
|
|
220
|
+
|
|
221
|
+
private var weeks: [(label: String, items: [Workout])] {
|
|
222
|
+
let grouped = Dictionary(grouping: workouts) { workout in
|
|
223
|
+
workout.startedAt.formatted(.dateTime.year().week())
|
|
319
224
|
}
|
|
320
|
-
return grouped
|
|
225
|
+
return grouped
|
|
226
|
+
.map { (label: $0.key, items: $0.value) }
|
|
227
|
+
.sorted { ($0.items.first?.startedAt ?? .distantPast) > ($1.items.first?.startedAt ?? .distantPast) }
|
|
321
228
|
}
|
|
322
229
|
|
|
323
230
|
var body: some View {
|
|
324
231
|
List {
|
|
325
|
-
ForEach(
|
|
326
|
-
Section(
|
|
327
|
-
ForEach(
|
|
328
|
-
TripRow(trip: trip)
|
|
329
|
-
}
|
|
232
|
+
ForEach(weeks, id: \.label) { week in
|
|
233
|
+
Section(week.label) {
|
|
234
|
+
ForEach(week.items) { Text($0.title) }
|
|
330
235
|
}
|
|
331
236
|
}
|
|
332
237
|
}
|
|
@@ -334,38 +239,32 @@ struct SectionedTripListView: View {
|
|
|
334
239
|
}
|
|
335
240
|
```
|
|
336
241
|
|
|
337
|
-
|
|
242
|
+
By a fixed set of buckets, skipping empty ones:
|
|
338
243
|
|
|
339
244
|
```swift
|
|
340
|
-
enum
|
|
341
|
-
case
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
private func trips(for status: TripStatus) -> [Trip] {
|
|
350
|
-
let now = Date.now
|
|
351
|
-
return trips.filter { trip in
|
|
352
|
-
switch status {
|
|
353
|
-
case .upcoming: trip.startDate > now
|
|
354
|
-
case .current: trip.startDate <= now && trip.endDate >= now
|
|
355
|
-
case .past: trip.endDate < now
|
|
356
|
-
}
|
|
245
|
+
enum WorkoutPhase: CaseIterable {
|
|
246
|
+
case planned, running, done
|
|
247
|
+
|
|
248
|
+
func includes(_ w: Workout, at now: Date) -> Bool {
|
|
249
|
+
switch self {
|
|
250
|
+
case .planned: w.startedAt > now
|
|
251
|
+
case .running: w.startedAt <= now && w.finishedAt == nil
|
|
252
|
+
case .done: w.finishedAt != nil
|
|
357
253
|
}
|
|
358
254
|
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
struct WorkoutsByPhase: View {
|
|
258
|
+
@Query(sort: \Workout.startedAt) private var workouts: [Workout]
|
|
359
259
|
|
|
360
260
|
var body: some View {
|
|
261
|
+
let now = Date()
|
|
361
262
|
List {
|
|
362
|
-
ForEach(
|
|
363
|
-
let
|
|
364
|
-
if !
|
|
365
|
-
Section(
|
|
366
|
-
ForEach(
|
|
367
|
-
TripRow(trip: trip)
|
|
368
|
-
}
|
|
263
|
+
ForEach(WorkoutPhase.allCases, id: \.self) { phase in
|
|
264
|
+
let items = workouts.filter { phase.includes($0, at: now) }
|
|
265
|
+
if !items.isEmpty {
|
|
266
|
+
Section(String(describing: phase)) {
|
|
267
|
+
ForEach(items) { Text($0.title) }
|
|
369
268
|
}
|
|
370
269
|
}
|
|
371
270
|
}
|
|
@@ -374,302 +273,195 @@ struct StatusSectionedView: View {
|
|
|
374
273
|
}
|
|
375
274
|
```
|
|
376
275
|
|
|
377
|
-
|
|
276
|
+
## Changing the query at runtime
|
|
378
277
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
### Filter + Sort Controlled by Parent
|
|
278
|
+
The parent owns the inputs; the child turns them into a query in `init`.
|
|
382
279
|
|
|
383
280
|
```swift
|
|
384
|
-
struct
|
|
385
|
-
@State private var
|
|
386
|
-
@State private var
|
|
281
|
+
struct WorkoutSearch: View {
|
|
282
|
+
@State private var text = ""
|
|
283
|
+
@State private var favoritesOnly = false
|
|
387
284
|
|
|
388
285
|
var body: some View {
|
|
389
286
|
NavigationStack {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
)
|
|
394
|
-
.searchable(text: $searchText)
|
|
395
|
-
.toolbar {
|
|
396
|
-
Toggle("Favorites", isOn: $showFavoritesOnly)
|
|
397
|
-
}
|
|
287
|
+
FilteredWorkouts(text: text, favoritesOnly: favoritesOnly)
|
|
288
|
+
.searchable(text: $text)
|
|
289
|
+
.toolbar { Toggle("Favorites", isOn: $favoritesOnly) }
|
|
398
290
|
}
|
|
399
291
|
}
|
|
400
292
|
}
|
|
401
293
|
|
|
402
|
-
struct
|
|
403
|
-
@Query private var
|
|
294
|
+
struct FilteredWorkouts: View {
|
|
295
|
+
@Query private var workouts: [Workout]
|
|
404
296
|
|
|
405
|
-
init(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
sort: [SortDescriptor(\.startDate, order: .reverse)]
|
|
297
|
+
init(text: String, favoritesOnly: Bool) {
|
|
298
|
+
_workouts = Query(
|
|
299
|
+
filter: #Predicate<Workout> {
|
|
300
|
+
(text.isEmpty || $0.title.localizedStandardContains(text))
|
|
301
|
+
&& (!favoritesOnly || $0.isFavorite)
|
|
302
|
+
},
|
|
303
|
+
sort: \.startedAt, order: .reverse
|
|
413
304
|
)
|
|
414
305
|
}
|
|
415
306
|
|
|
416
|
-
var body: some View {
|
|
417
|
-
List(trips) { trip in
|
|
418
|
-
NavigationLink(trip.name) {
|
|
419
|
-
TripDetailView(trip: trip)
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
307
|
+
var body: some View { List(workouts) { Text($0.title) } }
|
|
423
308
|
}
|
|
424
309
|
```
|
|
425
310
|
|
|
426
|
-
|
|
311
|
+
When more than the filter varies, build a whole descriptor:
|
|
427
312
|
|
|
428
313
|
```swift
|
|
429
|
-
struct
|
|
430
|
-
@Query private var
|
|
431
|
-
|
|
432
|
-
init(
|
|
433
|
-
destination: String?,
|
|
434
|
-
minDate: Date?,
|
|
435
|
-
sortKey: KeyPath<Trip, some Comparable>,
|
|
436
|
-
ascending: Bool,
|
|
437
|
-
limit: Int?
|
|
438
|
-
) {
|
|
439
|
-
var descriptor = FetchDescriptor<Trip>(
|
|
440
|
-
sortBy: [SortDescriptor(\.startDate, order: ascending ? .forward : .reverse)]
|
|
441
|
-
)
|
|
314
|
+
struct RecentWorkouts: View {
|
|
315
|
+
@Query private var workouts: [Workout]
|
|
442
316
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
317
|
+
init(since start: Date?, limit: Int?) {
|
|
318
|
+
var d = FetchDescriptor<Workout>(sortBy: [SortDescriptor(\.startedAt, order: .reverse)])
|
|
319
|
+
if let start {
|
|
320
|
+
d.predicate = #Predicate { $0.startedAt >= start }
|
|
447
321
|
}
|
|
448
|
-
|
|
449
322
|
if let limit {
|
|
450
|
-
|
|
323
|
+
d.fetchLimit = limit
|
|
451
324
|
}
|
|
452
|
-
|
|
453
|
-
_trips = Query(descriptor)
|
|
325
|
+
_workouts = Query(d)
|
|
454
326
|
}
|
|
455
327
|
|
|
456
|
-
var body: some View {
|
|
457
|
-
List(trips) { trip in
|
|
458
|
-
TripRow(trip: trip)
|
|
459
|
-
}
|
|
460
|
-
}
|
|
328
|
+
var body: some View { List(workouts) { Text($0.title) } }
|
|
461
329
|
}
|
|
462
330
|
```
|
|
463
331
|
|
|
464
|
-
|
|
332
|
+
## Fetching on a background actor
|
|
465
333
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
### Basic Background Fetch
|
|
334
|
+
Actor methods return `Sendable` results, such as identifiers or counts, never
|
|
335
|
+
model objects.
|
|
469
336
|
|
|
470
337
|
```swift
|
|
471
338
|
@ModelActor
|
|
472
|
-
actor
|
|
473
|
-
func
|
|
474
|
-
let
|
|
475
|
-
predicate: #Predicate { $0.
|
|
476
|
-
sortBy: [SortDescriptor(\.startDate)]
|
|
339
|
+
actor WorkoutStore {
|
|
340
|
+
func idsOfLongWorkouts(since start: Date) throws -> [PersistentIdentifier] {
|
|
341
|
+
let d = FetchDescriptor<Workout>(
|
|
342
|
+
predicate: #Predicate { $0.startedAt >= start && $0.minutes >= 60 }
|
|
477
343
|
)
|
|
478
|
-
return try modelContext.fetchIdentifiers(
|
|
344
|
+
return try modelContext.fetchIdentifiers(d)
|
|
479
345
|
}
|
|
480
346
|
|
|
481
|
-
func
|
|
482
|
-
|
|
483
|
-
predicate: #Predicate<Trip> { trip in
|
|
484
|
-
trip.destination == destination
|
|
485
|
-
}
|
|
486
|
-
)
|
|
487
|
-
return try modelContext.fetchCount(descriptor)
|
|
347
|
+
func favoriteCount() throws -> Int {
|
|
348
|
+
try modelContext.fetchCount(FetchDescriptor<Workout>(predicate: #Predicate { $0.isFavorite }))
|
|
488
349
|
}
|
|
489
|
-
}
|
|
490
350
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
var body: some View {
|
|
497
|
-
Text("Upcoming: \(upcomingCount)")
|
|
498
|
-
.task {
|
|
499
|
-
let handler = TripDataHandler(
|
|
500
|
-
modelContainer: modelContext.container
|
|
501
|
-
)
|
|
502
|
-
upcomingCount = (try? await handler.fetchTripCount(
|
|
503
|
-
destination: "Paris"
|
|
504
|
-
)) ?? 0
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
### Background Import with Progress
|
|
511
|
-
|
|
512
|
-
```swift
|
|
513
|
-
@ModelActor
|
|
514
|
-
actor ImportHandler {
|
|
515
|
-
func importTrips(
|
|
516
|
-
_ records: [TripRecord],
|
|
517
|
-
progress: @Sendable (Int) -> Void
|
|
518
|
-
) throws -> Int {
|
|
519
|
-
var imported = 0
|
|
520
|
-
for (index, record) in records.enumerated() {
|
|
521
|
-
let trip = Trip(
|
|
522
|
-
name: record.name,
|
|
523
|
-
destination: record.destination,
|
|
524
|
-
startDate: record.startDate,
|
|
525
|
-
endDate: record.endDate
|
|
526
|
-
)
|
|
527
|
-
modelContext.insert(trip)
|
|
528
|
-
imported += 1
|
|
529
|
-
|
|
530
|
-
// Save periodically to flush memory
|
|
531
|
-
if index % 500 == 0 {
|
|
351
|
+
func importSessions(_ rows: [SessionRow], progress: @Sendable (Int) -> Void) throws {
|
|
352
|
+
for (offset, row) in rows.enumerated() {
|
|
353
|
+
modelContext.insert(Workout(title: row.title, startedAt: row.start,
|
|
354
|
+
minutes: row.minutes, calories: row.calories))
|
|
355
|
+
if offset.isMultiple(of: 500) && offset > 0 {
|
|
532
356
|
try modelContext.save()
|
|
533
|
-
progress(
|
|
357
|
+
progress(offset)
|
|
534
358
|
}
|
|
535
359
|
}
|
|
536
360
|
try modelContext.save()
|
|
537
|
-
|
|
361
|
+
progress(rows.count)
|
|
538
362
|
}
|
|
539
363
|
}
|
|
364
|
+
|
|
365
|
+
struct SessionRow: Sendable {
|
|
366
|
+
let title: String
|
|
367
|
+
let start: Date
|
|
368
|
+
let minutes: Int
|
|
369
|
+
let calories: Double
|
|
370
|
+
}
|
|
540
371
|
```
|
|
541
372
|
|
|
542
|
-
|
|
373
|
+
Saving every 500 rows keeps memory flat during a long import. Call the actor
|
|
374
|
+
from a view with the view's container:
|
|
543
375
|
|
|
544
376
|
```swift
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// Complex logic to find duplicates
|
|
549
|
-
let all = try modelContext.fetch(FetchDescriptor<Trip>())
|
|
550
|
-
var seen = Set<String>()
|
|
551
|
-
var duplicateIDs: [PersistentIdentifier] = []
|
|
552
|
-
for trip in all {
|
|
553
|
-
if seen.contains(trip.name) {
|
|
554
|
-
duplicateIDs.append(trip.persistentModelID)
|
|
555
|
-
}
|
|
556
|
-
seen.insert(trip.name)
|
|
557
|
-
}
|
|
558
|
-
return duplicateIDs
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// On MainActor, resolve IDs to objects
|
|
563
|
-
struct DuplicateReviewView: View {
|
|
564
|
-
@Environment(\.modelContext) private var modelContext
|
|
565
|
-
@State private var duplicates: [Trip] = []
|
|
377
|
+
struct FavoriteBadge: View {
|
|
378
|
+
@Environment(\.modelContext) private var context
|
|
379
|
+
@State private var count = 0
|
|
566
380
|
|
|
567
381
|
var body: some View {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
let handler = DataHandler(modelContainer: modelContext.container)
|
|
573
|
-
let ids = (try? await handler.findDuplicateIDs()) ?? []
|
|
574
|
-
duplicates = ids.compactMap { id in
|
|
575
|
-
modelContext.registeredModel(for: id) as Trip?
|
|
576
|
-
?? (try? modelContext.model(for: id) as? Trip)
|
|
382
|
+
Text("\(count)")
|
|
383
|
+
.task {
|
|
384
|
+
let store = WorkoutStore(modelContainer: context.container)
|
|
385
|
+
count = (try? await store.favoriteCount()) ?? 0
|
|
577
386
|
}
|
|
578
|
-
}
|
|
579
387
|
}
|
|
580
388
|
}
|
|
581
389
|
```
|
|
582
390
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
## Aggregate Queries
|
|
586
|
-
|
|
587
|
-
SwiftData does not provide built-in aggregate functions (SUM, AVG, etc.).
|
|
588
|
-
Compute aggregates using fetch + Swift computation.
|
|
589
|
-
|
|
590
|
-
### Count
|
|
391
|
+
Turning identifiers back into models on the main context: check the objects the
|
|
392
|
+
context already holds, and fetch by identifier otherwise.
|
|
591
393
|
|
|
592
394
|
```swift
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
395
|
+
@MainActor
|
|
396
|
+
func resolve(_ ids: [PersistentIdentifier], in context: ModelContext) throws -> [Workout] {
|
|
397
|
+
try ids.compactMap { id in
|
|
398
|
+
if let cached: Workout = context.registeredModel(for: id) {
|
|
399
|
+
return cached
|
|
400
|
+
}
|
|
401
|
+
var d = FetchDescriptor<Workout>(predicate: #Predicate { $0.persistentModelID == id })
|
|
402
|
+
d.fetchLimit = 1
|
|
403
|
+
return try context.fetch(d).first
|
|
404
|
+
}
|
|
405
|
+
}
|
|
596
406
|
```
|
|
597
407
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
let trips = try modelContext.fetch(FetchDescriptor<Trip>())
|
|
602
|
-
|
|
603
|
-
let totalBudget = trips.reduce(0.0) { $0 + $1.budget }
|
|
604
|
-
let averageBudget = trips.isEmpty ? 0 : totalBudget / Double(trips.count)
|
|
605
|
-
let maxBudget = trips.map(\.budget).max() ?? 0
|
|
606
|
-
let minBudget = trips.map(\.budget).min() ?? 0
|
|
607
|
-
```
|
|
408
|
+
`context.model(for:)` also exists, but it returns an unchecked instance that
|
|
409
|
+
fails on first access if the row was deleted; the fetch above returns `nil`
|
|
410
|
+
instead.
|
|
608
411
|
|
|
609
|
-
|
|
412
|
+
## Aggregates
|
|
610
413
|
|
|
611
|
-
Fetch
|
|
414
|
+
SwiftData has no SUM, AVG, MIN or MAX. Fetch what you need and compute in
|
|
415
|
+
Swift; use `fetchCount` for counts. Restrict `propertiesToFetch` so only the
|
|
416
|
+
column being aggregated is loaded.
|
|
612
417
|
|
|
613
418
|
```swift
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
let
|
|
419
|
+
func calorieStats(in context: ModelContext) throws -> (total: Double, average: Double, peak: Double?) {
|
|
420
|
+
var d = FetchDescriptor<Workout>()
|
|
421
|
+
d.propertiesToFetch = [\.calories]
|
|
422
|
+
let values = try context.fetch(d).map(\.calories)
|
|
423
|
+
let total = values.reduce(0, +)
|
|
424
|
+
let average = values.isEmpty ? 0 : total / Double(values.count)
|
|
425
|
+
return (total, average, values.max())
|
|
426
|
+
}
|
|
618
427
|
```
|
|
619
428
|
|
|
620
|
-
|
|
429
|
+
Heavy statistics belong on a `@ModelActor` that returns a `Sendable` struct:
|
|
621
430
|
|
|
622
431
|
```swift
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
let averageDuration: TimeInterval
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
func computeStats() throws -> TripStats {
|
|
632
|
-
let allTrips = try modelContext.fetch(FetchDescriptor<Trip>())
|
|
633
|
-
let favoriteCount = try modelContext.fetchCount(
|
|
634
|
-
FetchDescriptor<Trip>(predicate: #Predicate { $0.isFavorite == true })
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
let totalDuration = allTrips.reduce(0.0) { sum, trip in
|
|
638
|
-
sum + trip.endDate.timeIntervalSince(trip.startDate)
|
|
639
|
-
}
|
|
640
|
-
let avgDuration = allTrips.isEmpty ? 0 : totalDuration / Double(allTrips.count)
|
|
432
|
+
struct WeeklySummary: Sendable {
|
|
433
|
+
let sessions: Int
|
|
434
|
+
let minutes: Int
|
|
435
|
+
let longest: Int
|
|
436
|
+
}
|
|
641
437
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
438
|
+
extension WorkoutStore {
|
|
439
|
+
func summary(from start: Date) throws -> WeeklySummary {
|
|
440
|
+
let d = FetchDescriptor<Workout>(predicate: #Predicate { $0.startedAt >= start })
|
|
441
|
+
let rows = try modelContext.fetch(d)
|
|
442
|
+
return WeeklySummary(sessions: rows.count,
|
|
443
|
+
minutes: rows.reduce(0) { $0 + $1.minutes },
|
|
444
|
+
longest: rows.map(\.minutes).max() ?? 0)
|
|
647
445
|
}
|
|
648
446
|
}
|
|
649
447
|
```
|
|
650
448
|
|
|
651
|
-
|
|
449
|
+
## enumerate for large sets
|
|
652
450
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
Use `enumerate` instead of `fetch` when processing many records to keep memory
|
|
656
|
-
usage constant:
|
|
451
|
+
For big sets, `enumerate` keeps memory flat where `fetch` would load
|
|
452
|
+
everything. Save when the loop is done.
|
|
657
453
|
|
|
658
454
|
```swift
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
batchSize: 1000,
|
|
455
|
+
try context.enumerate(
|
|
456
|
+
FetchDescriptor<Workout>(),
|
|
457
|
+
batchSize: 1_000,
|
|
663
458
|
allowEscapingMutations: false
|
|
664
|
-
) {
|
|
665
|
-
|
|
666
|
-
trip.isProcessed = true
|
|
459
|
+
) { workout in
|
|
460
|
+
workout.calories = (workout.calories * 10).rounded() / 10
|
|
667
461
|
}
|
|
668
|
-
try
|
|
462
|
+
try context.save()
|
|
669
463
|
```
|
|
670
464
|
|
|
671
|
-
|
|
672
|
-
- `
|
|
673
|
-
|
|
674
|
-
block. Set to `true` only if mutations must persist beyond the block.
|
|
675
|
-
|
|
465
|
+
- `batchSize` defaults to 5,000; a smaller batch uses less memory.
|
|
466
|
+
- With `allowEscapingMutations: false`, objects are released after each batch.
|
|
467
|
+
Set it to `true` only when changes must survive past the block.
|