@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,825 +1,613 @@
|
|
|
1
|
-
# SwiftUI Animation
|
|
1
|
+
# Advanced SwiftUI Animation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Depth behind the main skill: custom curves, the full `Spring` API, unit
|
|
4
|
+
curves, richer phase and keyframe patterns, transactions, every transition and
|
|
5
|
+
symbol effect, Reduce Motion handling and performance.
|
|
5
6
|
|
|
6
7
|
## Contents
|
|
7
8
|
|
|
8
|
-
- [CustomAnimation
|
|
9
|
-
- [Spring
|
|
10
|
-
- [UnitCurve
|
|
11
|
-
- [PhaseAnimator
|
|
12
|
-
- [KeyframeAnimator
|
|
9
|
+
- [CustomAnimation (iOS 17+)](#customanimation-ios-17)
|
|
10
|
+
- [Spring, every initializer](#spring-every-initializer)
|
|
11
|
+
- [UnitCurve (iOS 17+)](#unitcurve-ios-17)
|
|
12
|
+
- [PhaseAnimator patterns](#phaseanimator-patterns)
|
|
13
|
+
- [KeyframeAnimator patterns](#keyframeanimator-patterns)
|
|
13
14
|
- [Transaction and TransactionKey](#transaction-and-transactionkey)
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [Reduce Motion
|
|
18
|
-
- [
|
|
15
|
+
- [Choosing an implicit scope](#choosing-an-implicit-scope)
|
|
16
|
+
- [Every transition (iOS 17+)](#every-transition-ios-17)
|
|
17
|
+
- [Every symbol effect](#every-symbol-effect)
|
|
18
|
+
- [Reduce Motion](#reduce-motion)
|
|
19
|
+
- [Performance](#performance)
|
|
19
20
|
|
|
20
|
-
## CustomAnimation
|
|
21
|
-
|
|
22
|
-
Create entirely custom animation curves by conforming to `CustomAnimation`.
|
|
21
|
+
## CustomAnimation (iOS 17+)
|
|
23
22
|
|
|
24
23
|
```swift
|
|
25
24
|
@preconcurrency protocol CustomAnimation: Hashable, Sendable
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
One requirement and two optional hooks:
|
|
29
28
|
|
|
30
29
|
```swift
|
|
30
|
+
// Required. Return the value at `time`, or nil once the animation is finished.
|
|
31
31
|
func animate<V: VectorArithmetic>(
|
|
32
|
-
value: V,
|
|
33
|
-
time: TimeInterval,
|
|
34
|
-
context: inout AnimationContext<V>
|
|
32
|
+
value: V, time: TimeInterval, context: inout AnimationContext<V>
|
|
35
33
|
) -> V?
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Return the interpolated value at the given time. Return `nil` when the
|
|
39
|
-
animation is complete.
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```swift
|
|
35
|
+
// Optional. Current velocity, used when another animation takes over.
|
|
44
36
|
func velocity<V: VectorArithmetic>(
|
|
45
|
-
value: V,
|
|
46
|
-
time: TimeInterval,
|
|
47
|
-
context: AnimationContext<V>
|
|
37
|
+
value: V, time: TimeInterval, context: AnimationContext<V>
|
|
48
38
|
) -> V?
|
|
49
39
|
|
|
40
|
+
// Optional. Return true to absorb a new animation instead of being replaced.
|
|
50
41
|
func shouldMerge<V: VectorArithmetic>(
|
|
51
|
-
previous: Animation,
|
|
52
|
-
value: V,
|
|
53
|
-
time: TimeInterval,
|
|
54
|
-
context: inout AnimationContext<V>
|
|
42
|
+
previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>
|
|
55
43
|
) -> Bool
|
|
56
44
|
```
|
|
57
45
|
|
|
58
|
-
|
|
46
|
+
`value` is the full distance to travel; the return value is how much of it has
|
|
47
|
+
been covered at `time`.
|
|
48
|
+
|
|
49
|
+
A back-out curve that overshoots slightly and then settles:
|
|
59
50
|
|
|
60
51
|
```swift
|
|
61
|
-
struct
|
|
62
|
-
|
|
52
|
+
struct OvershootAnimation: CustomAnimation {
|
|
53
|
+
var duration: TimeInterval
|
|
54
|
+
var tension: Double = 1.7
|
|
63
55
|
|
|
64
56
|
func animate<V: VectorArithmetic>(
|
|
65
|
-
value: V,
|
|
66
|
-
time: TimeInterval,
|
|
67
|
-
context: inout AnimationContext<V>
|
|
57
|
+
value: V, time: TimeInterval, context: inout AnimationContext<V>
|
|
68
58
|
) -> V? {
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
let
|
|
72
|
-
let progress: Double
|
|
73
|
-
if p < 0.5 {
|
|
74
|
-
progress = -(pow(2, 20 * p - 10) * s) / 2
|
|
75
|
-
} else {
|
|
76
|
-
progress = (pow(2, -20 * p + 10) * s) / 2 + 1
|
|
77
|
-
}
|
|
59
|
+
if time > duration { return nil }
|
|
60
|
+
let t = time / duration - 1
|
|
61
|
+
let progress = t * t * ((tension + 1) * t + tension) + 1
|
|
78
62
|
return value.scaled(by: progress)
|
|
79
63
|
}
|
|
80
64
|
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Ergonomic Extension Pattern
|
|
84
65
|
|
|
85
|
-
Expose custom animations as static members on `Animation`.
|
|
86
|
-
|
|
87
|
-
```swift
|
|
88
66
|
extension Animation {
|
|
89
|
-
static var
|
|
90
|
-
elastic(duration: 0.35)
|
|
91
|
-
}
|
|
67
|
+
static var overshoot: Animation { overshoot(duration: 0.4) }
|
|
92
68
|
|
|
93
|
-
static func
|
|
94
|
-
Animation(
|
|
69
|
+
static func overshoot(duration: TimeInterval) -> Animation {
|
|
70
|
+
Animation(OvershootAnimation(duration: duration))
|
|
95
71
|
}
|
|
96
72
|
}
|
|
97
73
|
|
|
98
|
-
//
|
|
99
|
-
withAnimation(.elastic(duration: 0.5)) { isActive.toggle() }
|
|
74
|
+
// withAnimation(.overshoot(duration: 0.5)) { panelOffset = 0 }
|
|
100
75
|
```
|
|
101
76
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| Type | Role |
|
|
105
|
-
|---|---|
|
|
106
|
-
| `AnimationContext<V>` | Carries environment and per-animation state |
|
|
107
|
-
| `AnimationState` | Key-value storage for persisted state |
|
|
108
|
-
| `AnimationStateKey` | Protocol for defining custom state keys |
|
|
109
|
-
|
|
110
|
-
## Spring Type -- All Initializer Variants
|
|
111
|
-
|
|
112
|
-
### Perceptual (Preferred)
|
|
77
|
+
Supporting types:
|
|
113
78
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- `duration` -- Perceptual duration controlling pace. Default `0.5`.
|
|
119
|
-
- `bounce` -- Bounciness. `0.0` = no bounce, `1.0` = undamped. Negative values
|
|
120
|
-
produce overdamped springs. Default `0.0`.
|
|
79
|
+
- `AnimationContext<V>`: the environment plus state private to one running
|
|
80
|
+
animation.
|
|
81
|
+
- `AnimationState`: a key-value store that persists across `animate` calls.
|
|
82
|
+
- `AnimationStateKey`: the protocol your own state keys conform to.
|
|
121
83
|
|
|
122
|
-
|
|
84
|
+
## Spring, every initializer
|
|
123
85
|
|
|
124
|
-
|
|
125
|
-
Spring(mass: 1.0, stiffness: 100.0, damping: 10.0, allowOverDamping: false)
|
|
126
|
-
```
|
|
86
|
+
### `Spring(duration:bounce:)`
|
|
127
87
|
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
130
|
-
- `damping` -- Friction-like drag force.
|
|
131
|
-
- `allowOverDamping` -- Permit damping ratio > 1. Default `false`.
|
|
88
|
+
- `duration`: perceived pace. Default `0.5`.
|
|
89
|
+
- `bounce`: `0.0` none, `1.0` undamped, below zero overdamped. Default `0.0`.
|
|
132
90
|
|
|
133
|
-
###
|
|
91
|
+
### `Spring(mass:stiffness:damping:allowOverDamping:)`
|
|
134
92
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
93
|
+
- `mass`: default `1.0`.
|
|
94
|
+
- `stiffness`: the spring constant.
|
|
95
|
+
- `damping`: the drag that slows it.
|
|
96
|
+
- `allowOverDamping`: permits a damping ratio above 1. Default `false`.
|
|
138
97
|
|
|
139
|
-
|
|
140
|
-
- `dampingRatio` -- Fraction of critical damping. `1.0` = critically damped.
|
|
98
|
+
### `Spring(response:dampingRatio:)`
|
|
141
99
|
|
|
142
|
-
|
|
100
|
+
- `response`: stiffness expressed as a rough duration in seconds.
|
|
101
|
+
- `dampingRatio`: how much of critical damping is applied, where `1.0` means
|
|
102
|
+
exactly critical.
|
|
143
103
|
|
|
144
|
-
|
|
145
|
-
Spring(settlingDuration: 1.0, dampingRatio: 0.8, epsilon: 0.001)
|
|
146
|
-
```
|
|
104
|
+
### `Spring(settlingDuration:dampingRatio:epsilon:)`
|
|
147
105
|
|
|
148
|
-
- `settlingDuration
|
|
149
|
-
- `
|
|
150
|
-
- `epsilon` -- Threshold for considering the spring at rest. Default `0.001`.
|
|
106
|
+
- `settlingDuration`: estimated time until it comes to rest.
|
|
107
|
+
- `epsilon`: how close to rest counts as at rest. Default `0.001`.
|
|
151
108
|
|
|
152
109
|
### Presets
|
|
153
110
|
|
|
154
111
|
```swift
|
|
155
|
-
Spring.smooth
|
|
156
|
-
Spring.smooth(duration: 0.
|
|
157
|
-
Spring.snappy
|
|
158
|
-
Spring.snappy(duration: 0.
|
|
159
|
-
Spring.bouncy
|
|
160
|
-
Spring.bouncy(duration: 0.
|
|
112
|
+
Spring.smooth
|
|
113
|
+
Spring.smooth(duration: 0.6)
|
|
114
|
+
Spring.snappy
|
|
115
|
+
Spring.snappy(duration: 0.35, extraBounce: 0.05)
|
|
116
|
+
Spring.bouncy
|
|
117
|
+
Spring.bouncy(duration: 0.55, extraBounce: 0.15)
|
|
161
118
|
```
|
|
162
119
|
|
|
163
|
-
###
|
|
120
|
+
### Asking a spring questions
|
|
164
121
|
|
|
165
122
|
```swift
|
|
166
|
-
let
|
|
167
|
-
let
|
|
168
|
-
let
|
|
169
|
-
let
|
|
123
|
+
let pad = Spring(duration: 0.45, bounce: 0.2)
|
|
124
|
+
let position = pad.value(target: 100.0, initialVelocity: 0, time: 0.1)
|
|
125
|
+
let speed = pad.velocity(target: 100.0, initialVelocity: 0, time: 0.1)
|
|
126
|
+
let restAt = pad.settlingDuration(target: 100.0, initialVelocity: 0, epsilon: 0.01)
|
|
170
127
|
```
|
|
171
128
|
|
|
172
|
-
|
|
129
|
+
Any spring can also be read in the other parameter systems through `mass`,
|
|
130
|
+
`stiffness`, `damping`, `response`, `dampingRatio` and `settlingDuration`.
|
|
131
|
+
For example, a spring with a 0.5 second duration and 0.3 bounce converts to
|
|
132
|
+
mass 1.0, stiffness about 157.9 and damping about 17.6.
|
|
173
133
|
|
|
174
|
-
|
|
175
|
-
let spring = Spring(duration: 0.5, bounce: 0.3)
|
|
176
|
-
// Access physical equivalents:
|
|
177
|
-
spring.mass // 1.0
|
|
178
|
-
spring.stiffness // 157.9
|
|
179
|
-
spring.damping // 17.6
|
|
180
|
-
spring.response
|
|
181
|
-
spring.dampingRatio
|
|
182
|
-
spring.settlingDuration
|
|
183
|
-
```
|
|
134
|
+
## UnitCurve (iOS 17+)
|
|
184
135
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Map input progress [0,1] to output progress [0,1]. Used with
|
|
136
|
+
A `UnitCurve` maps progress in [0, 1] to eased progress in [0, 1]. Use it with
|
|
188
137
|
`.timingCurve(_:duration:)`.
|
|
189
138
|
|
|
190
|
-
|
|
139
|
+
Built in: `.linear`, `.easeIn`, `.easeOut`, `.easeInOut`, `.circularEaseIn`,
|
|
140
|
+
`.circularEaseOut`, `.circularEaseInOut`.
|
|
191
141
|
|
|
192
|
-
|
|
193
|
-
UnitCurve.linear
|
|
194
|
-
UnitCurve.easeIn
|
|
195
|
-
UnitCurve.easeOut
|
|
196
|
-
UnitCurve.easeInOut
|
|
197
|
-
UnitCurve.circularEaseIn
|
|
198
|
-
UnitCurve.circularEaseOut
|
|
199
|
-
UnitCurve.circularEaseInOut
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Custom Bezier Curve
|
|
142
|
+
Custom:
|
|
203
143
|
|
|
204
144
|
```swift
|
|
205
|
-
UnitCurve.bezier(
|
|
206
|
-
startControlPoint: UnitPoint(x: 0.
|
|
207
|
-
endControlPoint: UnitPoint(x: 0.
|
|
145
|
+
let gentle = UnitCurve.bezier(
|
|
146
|
+
startControlPoint: UnitPoint(x: 0.25, y: 0.1),
|
|
147
|
+
endControlPoint: UnitPoint(x: 0.25, y: 1.0)
|
|
208
148
|
)
|
|
149
|
+
let halfway = gentle.value(at: 0.5)
|
|
150
|
+
let slope = gentle.velocity(at: 0.5)
|
|
151
|
+
let reversed = gentle.inverse // x and y swapped
|
|
209
152
|
```
|
|
210
153
|
|
|
211
|
-
|
|
154
|
+
In use:
|
|
212
155
|
|
|
213
156
|
```swift
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
curve.velocity(at: 0.5) // rate of change at midpoint
|
|
217
|
-
curve.inverse // swaps x and y components
|
|
157
|
+
.animation(.timingCurve(UnitCurve.circularEaseOut, duration: 0.35), value: isOpen)
|
|
158
|
+
.animation(.timingCurve(0.34, 1.56, 0.64, 1, duration: 0.45), value: isOpen)
|
|
218
159
|
```
|
|
219
160
|
|
|
220
|
-
|
|
161
|
+
## PhaseAnimator patterns
|
|
221
162
|
|
|
222
|
-
|
|
223
|
-
.animation(.timingCurve(UnitCurve.circularEaseIn, duration: 0.4), value: x)
|
|
224
|
-
|
|
225
|
-
// Cubic bezier control points
|
|
226
|
-
.animation(.timingCurve(0.68, -0.55, 0.27, 1.55, duration: 0.5), value: x)
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## PhaseAnimator Deep Patterns
|
|
230
|
-
|
|
231
|
-
### Multi-Phase with Complex State
|
|
163
|
+
### Phases that carry their own values
|
|
232
164
|
|
|
233
165
|
```swift
|
|
234
|
-
enum
|
|
235
|
-
case
|
|
236
|
-
|
|
237
|
-
var scale: Double {
|
|
238
|
-
switch self {
|
|
239
|
-
case .ready: 1.0
|
|
240
|
-
case .loading: 0.9
|
|
241
|
-
case .spinning: 1.0
|
|
242
|
-
case .complete: 1.1
|
|
243
|
-
}
|
|
244
|
-
}
|
|
166
|
+
enum UploadPhase: CaseIterable {
|
|
167
|
+
case idle, preparing, sending, done
|
|
245
168
|
|
|
246
|
-
var
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
default: .zero
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
var opacity: Double {
|
|
254
|
-
self == .loading ? 0.7 : 1.0
|
|
255
|
-
}
|
|
169
|
+
var scale: Double { self == .preparing ? 0.9 : 1 }
|
|
170
|
+
var spin: Angle { self == .sending ? .degrees(360) : .zero }
|
|
171
|
+
var opacity: Double { self == .idle ? 0.6 : 1 }
|
|
256
172
|
}
|
|
257
173
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
case .loading: .easeIn(duration: 0.15)
|
|
270
|
-
case .spinning: .linear(duration: 0.6)
|
|
271
|
-
case .complete: .spring(duration: 0.3, bounce: 0.4)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
174
|
+
PhaseAnimator(UploadPhase.allCases) { phase in
|
|
175
|
+
Image(systemName: "arrow.up.circle.fill")
|
|
176
|
+
.scaleEffect(phase.scale)
|
|
177
|
+
.rotationEffect(phase.spin)
|
|
178
|
+
.opacity(phase.opacity)
|
|
179
|
+
} animation: { phase in
|
|
180
|
+
switch phase {
|
|
181
|
+
case .idle: .smooth
|
|
182
|
+
case .preparing: .easeIn(duration: 0.2)
|
|
183
|
+
case .sending: .linear(duration: 0.6)
|
|
184
|
+
case .done: .spring(duration: 0.45, bounce: 0.35)
|
|
274
185
|
}
|
|
275
186
|
}
|
|
276
187
|
```
|
|
277
188
|
|
|
278
|
-
###
|
|
279
|
-
|
|
280
|
-
Run through all phases once each time the trigger value changes.
|
|
189
|
+
### One pass per trigger
|
|
281
190
|
|
|
282
191
|
```swift
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
.phaseAnimator(
|
|
291
|
-
[false, true, false],
|
|
292
|
-
trigger: feedbackTrigger
|
|
293
|
-
) { content, phase in
|
|
294
|
-
content.scaleEffect(phase ? 1.5 : 1.0)
|
|
295
|
-
} animation: { _ in
|
|
296
|
-
.spring(duration: 0.25, bounce: 0.5)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
.buttonStyle(.plain)
|
|
192
|
+
@State private var taps = 0
|
|
193
|
+
|
|
194
|
+
Button("Like") { taps += 1 }
|
|
195
|
+
.phaseAnimator([false, true, false], trigger: taps) { content, pressed in
|
|
196
|
+
content.scaleEffect(pressed ? 1.2 : 1)
|
|
197
|
+
} animation: { _ in
|
|
198
|
+
.spring(duration: 0.3, bounce: 0.45)
|
|
300
199
|
}
|
|
301
|
-
}
|
|
302
200
|
```
|
|
303
201
|
|
|
304
|
-
###
|
|
202
|
+
### Plain values as phases
|
|
203
|
+
|
|
204
|
+
Phases do not have to be an enum:
|
|
305
205
|
|
|
306
206
|
```swift
|
|
307
|
-
|
|
308
|
-
.phaseAnimator([0.
|
|
309
|
-
content.opacity(
|
|
310
|
-
} animation: { _ in
|
|
207
|
+
Circle()
|
|
208
|
+
.phaseAnimator([0.2, 1.0, 0.2]) { content, level in
|
|
209
|
+
content.opacity(level)
|
|
210
|
+
} animation: { _ in
|
|
211
|
+
.easeInOut(duration: 0.8)
|
|
212
|
+
}
|
|
311
213
|
```
|
|
312
214
|
|
|
313
|
-
## KeyframeAnimator
|
|
215
|
+
## KeyframeAnimator patterns
|
|
314
216
|
|
|
315
|
-
###
|
|
217
|
+
### Four tracks
|
|
316
218
|
|
|
317
219
|
```swift
|
|
318
|
-
struct
|
|
319
|
-
var
|
|
320
|
-
var scale
|
|
321
|
-
var
|
|
322
|
-
var
|
|
220
|
+
struct DropValues {
|
|
221
|
+
var y = -60.0
|
|
222
|
+
var scale = 0.8
|
|
223
|
+
var tilt = Angle.zero
|
|
224
|
+
var opacity = 0.0
|
|
323
225
|
}
|
|
324
226
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
KeyframeTrack(\.scale) {
|
|
350
|
-
CubicKeyframe(1.3, duration: 0.2)
|
|
351
|
-
CubicKeyframe(0.95, duration: 0.15)
|
|
352
|
-
SpringKeyframe(1.0, duration: 0.3)
|
|
353
|
-
}
|
|
354
|
-
KeyframeTrack(\.rotation) {
|
|
355
|
-
LinearKeyframe(.degrees(-5), duration: 0.1)
|
|
356
|
-
LinearKeyframe(.degrees(5), duration: 0.1)
|
|
357
|
-
SpringKeyframe(.zero, duration: 0.2)
|
|
358
|
-
}
|
|
359
|
-
KeyframeTrack(\.opacity) {
|
|
360
|
-
MoveKeyframe(1.0)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
227
|
+
Image(systemName: "gift.fill")
|
|
228
|
+
.keyframeAnimator(initialValue: DropValues(), trigger: revealCount) { view, v in
|
|
229
|
+
view
|
|
230
|
+
.offset(y: v.y)
|
|
231
|
+
.scaleEffect(v.scale)
|
|
232
|
+
.rotationEffect(v.tilt)
|
|
233
|
+
.opacity(v.opacity)
|
|
234
|
+
} keyframes: { _ in
|
|
235
|
+
KeyframeTrack(\.y) {
|
|
236
|
+
CubicKeyframe(0, duration: 0.3)
|
|
237
|
+
SpringKeyframe(-12, duration: 0.15)
|
|
238
|
+
SpringKeyframe(0, duration: 0.25)
|
|
239
|
+
}
|
|
240
|
+
KeyframeTrack(\.scale) {
|
|
241
|
+
CubicKeyframe(1.1, duration: 0.3)
|
|
242
|
+
SpringKeyframe(1.0, duration: 0.4)
|
|
243
|
+
}
|
|
244
|
+
KeyframeTrack(\.tilt) {
|
|
245
|
+
LinearKeyframe(.degrees(-8), duration: 0.15)
|
|
246
|
+
LinearKeyframe(.degrees(8), duration: 0.15)
|
|
247
|
+
SpringKeyframe(.zero, duration: 0.3)
|
|
248
|
+
}
|
|
249
|
+
KeyframeTrack(\.opacity) {
|
|
250
|
+
MoveKeyframe(1.0)
|
|
363
251
|
}
|
|
364
|
-
.buttonStyle(.plain)
|
|
365
252
|
}
|
|
366
|
-
}
|
|
367
253
|
```
|
|
368
254
|
|
|
369
|
-
###
|
|
255
|
+
### Looping
|
|
370
256
|
|
|
371
257
|
```swift
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
258
|
+
struct Glow {
|
|
259
|
+
var scale = 1.0
|
|
260
|
+
var opacity = 0.5
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
KeyframeAnimator(initialValue: Glow(), repeating: true) { glow in
|
|
376
264
|
Circle()
|
|
377
|
-
.fill(.
|
|
378
|
-
.
|
|
379
|
-
.
|
|
380
|
-
.opacity(value.opacity)
|
|
265
|
+
.fill(.yellow)
|
|
266
|
+
.scaleEffect(glow.scale)
|
|
267
|
+
.opacity(glow.opacity)
|
|
381
268
|
} keyframes: { _ in
|
|
382
269
|
KeyframeTrack(\.scale) {
|
|
383
|
-
CubicKeyframe(1.
|
|
384
|
-
CubicKeyframe(1
|
|
270
|
+
CubicKeyframe(1.15, duration: 0.6)
|
|
271
|
+
CubicKeyframe(1, duration: 0.6)
|
|
385
272
|
}
|
|
386
273
|
KeyframeTrack(\.opacity) {
|
|
387
|
-
CubicKeyframe(0.
|
|
388
|
-
CubicKeyframe(
|
|
274
|
+
CubicKeyframe(0.95, duration: 0.6)
|
|
275
|
+
CubicKeyframe(0.5, duration: 0.6)
|
|
389
276
|
}
|
|
390
277
|
}
|
|
391
278
|
```
|
|
392
279
|
|
|
393
|
-
### Keyframe
|
|
280
|
+
### Keyframe types
|
|
394
281
|
|
|
395
|
-
|
|
|
282
|
+
| Keyframe | Motion | Typical use |
|
|
396
283
|
|---|---|---|
|
|
397
|
-
| `LinearKeyframe(value, duration:)` |
|
|
398
|
-
| `CubicKeyframe(value, duration:)` |
|
|
399
|
-
| `SpringKeyframe(value, duration
|
|
400
|
-
| `MoveKeyframe(value)` |
|
|
284
|
+
| `LinearKeyframe(value, duration:)` | straight-line interpolation | steady movement |
|
|
285
|
+
| `CubicKeyframe(value, duration:)` | cubic Bezier | smooth easing |
|
|
286
|
+
| `SpringKeyframe(value, duration:spring:)` | driven by a spring | settling into place |
|
|
287
|
+
| `MoveKeyframe(value)` | instant jump | resetting a value |
|
|
401
288
|
|
|
402
|
-
###
|
|
289
|
+
### Evaluating keyframes yourself
|
|
403
290
|
|
|
404
291
|
```swift
|
|
405
|
-
let timeline = KeyframeTimeline(initialValue:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
CubicKeyframe(1.0, duration: 0.4)
|
|
409
|
-
}
|
|
292
|
+
let timeline = KeyframeTimeline(initialValue: 0.0) {
|
|
293
|
+
CubicKeyframe(20, duration: 0.3)
|
|
294
|
+
SpringKeyframe(0, duration: 0.5)
|
|
410
295
|
}
|
|
411
|
-
|
|
412
|
-
let
|
|
413
|
-
let valueAtHalf = timeline.value(time: totalDuration / 2)
|
|
296
|
+
let total = timeline.duration // 0.8
|
|
297
|
+
let atHalf = timeline.value(time: 0.5)
|
|
414
298
|
```
|
|
415
299
|
|
|
416
300
|
## Transaction and TransactionKey
|
|
417
301
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
A `Transaction` carries the animation context for a state change. Every
|
|
421
|
-
`withAnimation` call creates a transaction internally.
|
|
302
|
+
Every state change travels with a `Transaction` that says how to animate it;
|
|
303
|
+
`withAnimation` creates one behind the scenes.
|
|
422
304
|
|
|
423
305
|
```swift
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
withTransaction(transaction) {
|
|
427
|
-
isExpanded = true
|
|
428
|
-
}
|
|
306
|
+
let pinning = Transaction(animation: .snappy)
|
|
307
|
+
withTransaction(pinning) { isPinned = true }
|
|
429
308
|
```
|
|
430
309
|
|
|
431
|
-
|
|
310
|
+
Removing the incoming animation for one subtree:
|
|
432
311
|
|
|
433
312
|
```swift
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.transaction { transaction in
|
|
437
|
-
transaction.animation = nil
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Override the scoped transaction animation when a value changes
|
|
441
|
-
SomeView()
|
|
442
|
-
.transaction(value: selectedTab) { transaction in
|
|
443
|
-
transaction.animation = .smooth(duration: 0.3)
|
|
444
|
-
}
|
|
313
|
+
BadgeView(count: count)
|
|
314
|
+
.transaction { $0.animation = nil }
|
|
445
315
|
```
|
|
446
316
|
|
|
447
|
-
|
|
317
|
+
Overriding only when a particular value changes:
|
|
318
|
+
|
|
319
|
+
```swift
|
|
320
|
+
ChartView(points: points)
|
|
321
|
+
.transaction(value: selectedRange) { $0.animation = .smooth }
|
|
322
|
+
```
|
|
448
323
|
|
|
449
|
-
|
|
324
|
+
### Custom transaction metadata
|
|
450
325
|
|
|
451
326
|
```swift
|
|
452
|
-
struct
|
|
327
|
+
struct DragDrivenKey: TransactionKey {
|
|
453
328
|
static let defaultValue = false
|
|
454
329
|
}
|
|
455
330
|
|
|
456
331
|
extension Transaction {
|
|
457
|
-
var
|
|
458
|
-
get { self[
|
|
459
|
-
set { self[
|
|
332
|
+
var isDragDriven: Bool {
|
|
333
|
+
get { self[DragDrivenKey.self] }
|
|
334
|
+
set { self[DragDrivenKey.self] = newValue }
|
|
460
335
|
}
|
|
461
336
|
}
|
|
462
337
|
|
|
463
|
-
//
|
|
338
|
+
// During a drag:
|
|
464
339
|
var transaction = Transaction(animation: .interactiveSpring)
|
|
465
|
-
transaction.
|
|
466
|
-
withTransaction(transaction) {
|
|
340
|
+
transaction.isDragDriven = true
|
|
341
|
+
withTransaction(transaction) { cardOffset = dragTranslation }
|
|
467
342
|
```
|
|
468
343
|
|
|
469
|
-
|
|
344
|
+
A view downstream can read `transaction.isDragDriven` inside
|
|
345
|
+
`.transaction { }` and choose a different animation.
|
|
346
|
+
|
|
347
|
+
### Scoped override
|
|
348
|
+
|
|
349
|
+
`.transaction(_:body:)` applies the change only to the modifiers in its body:
|
|
470
350
|
|
|
471
351
|
```swift
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
content.scaleEffect(scale)
|
|
352
|
+
Card()
|
|
353
|
+
.transaction { t in t.animation = .bouncy } body: { view in
|
|
354
|
+
view.scaleEffect(zoom)
|
|
476
355
|
}
|
|
477
356
|
```
|
|
478
357
|
|
|
479
|
-
|
|
358
|
+
## Choosing an implicit scope
|
|
480
359
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
360
|
+
| Need | Use |
|
|
361
|
+
|---|---|
|
|
362
|
+
| Animate some modifiers, not others | `.animation(_:body:)` |
|
|
363
|
+
| One value drives every animatable modifier | `.animation(_:value:)` |
|
|
364
|
+
| Adjust the transaction for a region rather than attach one animation | `.transaction(_:body:)` |
|
|
485
365
|
|
|
486
366
|
```swift
|
|
487
|
-
|
|
367
|
+
NoteCard(note: note)
|
|
488
368
|
.animation(.smooth) { content in
|
|
489
369
|
content
|
|
490
|
-
.scaleEffect(isExpanded ? 1.05 : 1
|
|
491
|
-
.shadow(radius: isExpanded ? 12 :
|
|
370
|
+
.scaleEffect(isExpanded ? 1.05 : 1)
|
|
371
|
+
.shadow(radius: isExpanded ? 12 : 2)
|
|
492
372
|
}
|
|
493
373
|
```
|
|
494
374
|
|
|
495
|
-
##
|
|
496
|
-
|
|
497
|
-
### Built-in Transitions
|
|
375
|
+
## Every transition (iOS 17+)
|
|
498
376
|
|
|
499
|
-
| Transition |
|
|
500
|
-
|
|
501
|
-
| `.opacity` |
|
|
502
|
-
| `.slide` |
|
|
503
|
-
| `.scale` |
|
|
504
|
-
| `.scale(
|
|
505
|
-
| `.move(edge:)` |
|
|
506
|
-
| `.push(from:)` |
|
|
507
|
-
| `.offset(
|
|
508
|
-
| `.offset(x:y:)` |
|
|
509
|
-
| `.identity` |
|
|
510
|
-
| `.blurReplace` |
|
|
511
|
-
| `.blurReplace(
|
|
512
|
-
| `.symbolEffect` |
|
|
513
|
-
| `.symbolEffect(
|
|
514
|
-
|
|
515
|
-
|
|
377
|
+
| Transition | Effect |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `.opacity` | fade |
|
|
380
|
+
| `.slide` | in from the leading edge, out toward the trailing edge |
|
|
381
|
+
| `.scale` | grows from zero |
|
|
382
|
+
| `.scale(0.5, anchor: .bottom)` | grows from a size and anchor |
|
|
383
|
+
| `.move(edge: .leading)` | slides from an edge |
|
|
384
|
+
| `.push(from: .trailing)` | push with a fade from an edge |
|
|
385
|
+
| `.offset(CGSize(width: 0, height: 40))` | shifts by a size |
|
|
386
|
+
| `.offset(x: 0, y: 40)` | shifts by components |
|
|
387
|
+
| `.identity` | no visual change |
|
|
388
|
+
| `.blurReplace` | blur combined with scale |
|
|
389
|
+
| `.blurReplace(.downUp)` | configurable blur replace |
|
|
390
|
+
| `.symbolEffect` | the default symbol transition |
|
|
391
|
+
| `.symbolEffect(.appear)` | a chosen symbol effect, with options |
|
|
392
|
+
|
|
393
|
+
Combining and splitting:
|
|
516
394
|
|
|
517
395
|
```swift
|
|
518
|
-
// Slide + fade
|
|
519
396
|
.transition(.slide.combined(with: .opacity))
|
|
520
|
-
|
|
521
|
-
// Move from top + scale
|
|
522
|
-
.transition(.move(edge: .top).combined(with: .scale))
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### Asymmetric Transitions
|
|
526
|
-
|
|
527
|
-
Different animation for insertion vs removal.
|
|
528
|
-
|
|
529
|
-
```swift
|
|
397
|
+
.transition(.scale.combined(with: .move(edge: .leading)))
|
|
530
398
|
.transition(.asymmetric(
|
|
531
|
-
insertion: .
|
|
532
|
-
removal: .
|
|
399
|
+
insertion: .opacity.combined(with: .push(from: .top)),
|
|
400
|
+
removal: .opacity.combined(with: .scale(scale: 0.8))
|
|
533
401
|
))
|
|
534
402
|
```
|
|
535
403
|
|
|
536
|
-
|
|
404
|
+
`.asymmetric(insertion:removal:)` exists only on `AnyTransition`, so every
|
|
405
|
+
argument inside it resolves to the `AnyTransition` members, where the sized
|
|
406
|
+
scale is spelled `.scale(scale:anchor:)`. Elsewhere the `Transition` form
|
|
407
|
+
`.scale(0.8)` works.
|
|
408
|
+
|
|
409
|
+
### A custom Transition
|
|
537
410
|
|
|
538
411
|
```swift
|
|
539
|
-
struct
|
|
412
|
+
struct FlipIn: Transition {
|
|
540
413
|
func body(content: Content, phase: TransitionPhase) -> some View {
|
|
541
414
|
content
|
|
542
|
-
.
|
|
415
|
+
.rotation3DEffect(.degrees(phase.isIdentity ? 0 : 90), axis: (x: 1, y: 0, z: 0))
|
|
543
416
|
.opacity(phase.isIdentity ? 1 : 0)
|
|
544
417
|
}
|
|
545
418
|
}
|
|
546
419
|
|
|
547
|
-
extension
|
|
548
|
-
static var
|
|
549
|
-
.init(RotateTransition())
|
|
550
|
-
}
|
|
420
|
+
extension Transition where Self == FlipIn {
|
|
421
|
+
static var flipIn: FlipIn { FlipIn() }
|
|
551
422
|
}
|
|
423
|
+
|
|
424
|
+
// .transition(.flipIn)
|
|
552
425
|
```
|
|
553
426
|
|
|
554
|
-
|
|
427
|
+
Where an API still wants the older type-erased value, wrap it:
|
|
428
|
+
`AnyTransition(FlipIn())`, for example behind
|
|
429
|
+
`extension AnyTransition { static var flipIn: AnyTransition { AnyTransition(FlipIn()) } }`.
|
|
555
430
|
|
|
556
|
-
|
|
557
|
-
enum TransitionPhase {
|
|
558
|
-
case willAppear // View is about to be inserted
|
|
559
|
-
case identity // View is fully presented
|
|
560
|
-
case didDisappear // View is being removed
|
|
561
|
-
}
|
|
431
|
+
`TransitionPhase`:
|
|
562
432
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
433
|
+
- `.willAppear`: about to be inserted.
|
|
434
|
+
- `.identity`: fully on screen.
|
|
435
|
+
- `.didDisappear`: being removed.
|
|
436
|
+
- `isIdentity`: true only in the fully presented phase.
|
|
566
437
|
|
|
567
|
-
###
|
|
438
|
+
### Pinning an animation to a transition
|
|
568
439
|
|
|
569
440
|
```swift
|
|
570
441
|
.transition(
|
|
571
|
-
.
|
|
442
|
+
.scale(0.9)
|
|
572
443
|
.combined(with: .opacity)
|
|
573
|
-
.animation(.spring(duration: 0.
|
|
444
|
+
.animation(.spring(duration: 0.3, bounce: 0.15))
|
|
574
445
|
)
|
|
575
446
|
```
|
|
576
447
|
|
|
577
|
-
##
|
|
578
|
-
|
|
579
|
-
### Discrete Effects (trigger with `value:`)
|
|
580
|
-
|
|
581
|
-
| Effect | Scope | Direction |
|
|
582
|
-
|---|---|---|
|
|
583
|
-
| `.bounce` | `.byLayer`, `.wholeSymbol` | -- |
|
|
584
|
-
| `.wiggle` | `.byLayer`, `.wholeSymbol` | `.up`, `.down`, `.left`, `.right`, `.forward`, `.backward`, `.clockwise`, `.counterClockwise`, `.custom(angle:)` |
|
|
585
|
-
|
|
586
|
-
```swift
|
|
587
|
-
Image(systemName: "bell.fill")
|
|
588
|
-
.symbolEffect(.bounce.byLayer, value: count)
|
|
589
|
-
|
|
590
|
-
Image(systemName: "arrow.left.arrow.right")
|
|
591
|
-
.symbolEffect(.wiggle.left, value: swapCount)
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
### Indefinite Effects (toggle with `isActive:`)
|
|
595
|
-
|
|
596
|
-
| Effect | Scope | Direction |
|
|
597
|
-
|---|---|---|
|
|
598
|
-
| `.pulse` | `.byLayer`, `.wholeSymbol` | -- |
|
|
599
|
-
| `.variableColor` | `.byLayer`, `.wholeSymbol` | Chaining: `.cumulative`/`.iterative`, `.reversing`/`.nonReversing`, `.dimInactiveLayers`/`.hideInactiveLayers` |
|
|
600
|
-
| `.scale` | `.byLayer`, `.wholeSymbol` | `.up`, `.down` |
|
|
601
|
-
| `.breathe` | `.byLayer`, `.wholeSymbol` | -- |
|
|
602
|
-
| `.rotate` | `.byLayer`, `.wholeSymbol` | `.clockwise`, `.counterClockwise` |
|
|
603
|
-
|
|
604
|
-
```swift
|
|
605
|
-
Image(systemName: "wifi")
|
|
606
|
-
.symbolEffect(.pulse.byLayer, isActive: isConnecting)
|
|
607
|
-
|
|
608
|
-
Image(systemName: "gear")
|
|
609
|
-
.symbolEffect(.rotate.clockwise, isActive: isProcessing)
|
|
610
|
-
|
|
611
|
-
Image(systemName: "speaker.wave.3.fill")
|
|
612
|
-
.symbolEffect(
|
|
613
|
-
.variableColor.cumulative.nonReversing.dimInactiveLayers,
|
|
614
|
-
options: .repeating,
|
|
615
|
-
isActive: isPlaying
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
Image(systemName: "magnifyingglass")
|
|
619
|
-
.symbolEffect(.scale.up, isActive: isHighlighted)
|
|
620
|
-
|
|
621
|
-
Image(systemName: "heart.fill")
|
|
622
|
-
.symbolEffect(.breathe, isActive: isFavorite)
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
### Transition Effects (appear/disappear)
|
|
626
|
-
|
|
627
|
-
```swift
|
|
628
|
-
Image(systemName: "checkmark.circle.fill")
|
|
629
|
-
.symbolEffect(.appear, isActive: showCheck)
|
|
630
|
-
|
|
631
|
-
Image(systemName: "xmark.circle")
|
|
632
|
-
.symbolEffect(.disappear, isActive: shouldHide)
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
### Content Transition Effects (replace)
|
|
448
|
+
## Every symbol effect
|
|
636
449
|
|
|
637
|
-
|
|
638
|
-
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
|
|
639
|
-
.contentTransition(.symbolEffect(.replace.downUp))
|
|
450
|
+
### Discrete, triggered by `value:`
|
|
640
451
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
Replace directions: `.downUp`, `.offUp`, `.upUp`.
|
|
647
|
-
|
|
648
|
-
### SymbolEffectOptions
|
|
452
|
+
- `.bounce`: scopes `.byLayer` and `.wholeSymbol`; no direction.
|
|
453
|
+
- `.wiggle` (iOS 18): scopes, plus directions `.up`, `.down`, `.left`,
|
|
454
|
+
`.right`, `.forward`, `.backward`, `.clockwise`, `.counterClockwise`,
|
|
455
|
+
`.custom(angle:)`.
|
|
649
456
|
|
|
650
457
|
```swift
|
|
651
|
-
.symbolEffect(.
|
|
652
|
-
.symbolEffect(.
|
|
653
|
-
.symbolEffect(.pulse, options: .nonRepeating, isActive: true)
|
|
654
|
-
.symbolEffect(.bounce, options: .repeat(3), value: count)
|
|
655
|
-
.symbolEffect(.pulse, options: .speed(2.0), isActive: true)
|
|
656
|
-
|
|
657
|
-
// RepeatBehavior
|
|
658
|
-
.symbolEffect(.bounce, options: .repeat(.periodic(3, delay: 0.5)), value: count)
|
|
659
|
-
.symbolEffect(.pulse, options: .repeat(.continuous), isActive: true)
|
|
458
|
+
Image(systemName: "tray.and.arrow.down.fill").symbolEffect(.bounce.byLayer, value: downloads)
|
|
459
|
+
Image(systemName: "hand.wave.fill").symbolEffect(.wiggle.left, value: greetings)
|
|
660
460
|
```
|
|
661
461
|
|
|
662
|
-
###
|
|
663
|
-
|
|
664
|
-
```swift
|
|
665
|
-
Image(systemName: "star.fill")
|
|
666
|
-
.symbolEffect(.pulse, isActive: true)
|
|
667
|
-
.symbolEffectsRemoved(reduceMotion)
|
|
668
|
-
```
|
|
462
|
+
### Indefinite, controlled by `isActive:`
|
|
669
463
|
|
|
670
|
-
|
|
464
|
+
- `.pulse`
|
|
465
|
+
- `.variableColor`: chain `.cumulative` or `.iterative`, `.reversing` or
|
|
466
|
+
`.nonReversing`, `.dimInactiveLayers` or `.hideInactiveLayers`.
|
|
467
|
+
- `.scale`: `.up` or `.down`.
|
|
468
|
+
- `.breathe` (iOS 18)
|
|
469
|
+
- `.rotate` (iOS 18): `.clockwise` or `.counterClockwise`.
|
|
671
470
|
|
|
672
|
-
|
|
471
|
+
All of them take `.byLayer` or `.wholeSymbol`.
|
|
673
472
|
|
|
674
473
|
```swift
|
|
675
|
-
|
|
474
|
+
Image(systemName: "antenna.radiowaves.left.and.right").symbolEffect(.pulse.byLayer, isActive: isLive)
|
|
475
|
+
Image(systemName: "gearshape.fill").symbolEffect(.rotate.clockwise, isActive: isSyncing)
|
|
476
|
+
Image(systemName: "chart.bar.fill")
|
|
477
|
+
.symbolEffect(.variableColor.cumulative.nonReversing.dimInactiveLayers,
|
|
478
|
+
options: .repeating, isActive: isLoading)
|
|
479
|
+
Image(systemName: "plus.circle").symbolEffect(.scale.up, isActive: isHighlighted)
|
|
480
|
+
Image(systemName: "moon.zzz.fill").symbolEffect(.breathe, isActive: isSleeping)
|
|
676
481
|
```
|
|
677
482
|
|
|
678
|
-
###
|
|
483
|
+
### Appear and disappear
|
|
679
484
|
|
|
680
485
|
```swift
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
486
|
+
Image(systemName: "checkmark.seal.fill").symbolEffect(.appear, isActive: isVerified)
|
|
487
|
+
Image(systemName: "exclamationmark.triangle").symbolEffect(.disappear, isActive: isResolved)
|
|
684
488
|
```
|
|
685
489
|
|
|
686
|
-
###
|
|
687
|
-
|
|
688
|
-
Replace bouncy/spring with crossfade when reduce motion is on.
|
|
689
|
-
|
|
690
|
-
```swift
|
|
691
|
-
withAnimation(reduceMotion ? .easeInOut(duration: 0.2) : .spring(duration: 0.4, bounce: 0.3)) {
|
|
692
|
-
selectedTab = newTab
|
|
693
|
-
}
|
|
694
|
-
```
|
|
490
|
+
### Replace
|
|
695
491
|
|
|
696
|
-
|
|
492
|
+
Replacement runs as a content transition:
|
|
697
493
|
|
|
698
494
|
```swift
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// CORRECT: Use trigger-based or skip entirely
|
|
703
|
-
if !reduceMotion {
|
|
704
|
-
PhaseAnimator(phases) { phase in /* ... */ }
|
|
705
|
-
} else {
|
|
706
|
-
StaticView()
|
|
707
|
-
}
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
### Pattern 4: Symbol Effects
|
|
711
|
-
|
|
712
|
-
```swift
|
|
713
|
-
Image(systemName: "wifi")
|
|
714
|
-
.symbolEffect(.pulse, isActive: isSearching)
|
|
715
|
-
.symbolEffectsRemoved(reduceMotion)
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
### Pattern 5: Reusable Helper
|
|
719
|
-
|
|
720
|
-
```swift
|
|
721
|
-
extension Animation {
|
|
722
|
-
static func adaptive(
|
|
723
|
-
_ animation: Animation,
|
|
724
|
-
reduceMotion: Bool
|
|
725
|
-
) -> Animation? {
|
|
726
|
-
reduceMotion ? nil : animation
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Usage
|
|
731
|
-
withAnimation(.adaptive(.bouncy, reduceMotion: reduceMotion)) {
|
|
732
|
-
isVisible = true
|
|
733
|
-
}
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
## Animation Performance Tips
|
|
737
|
-
|
|
738
|
-
### Keep Content Closures Light
|
|
739
|
-
|
|
740
|
-
The `content` closure in `KeyframeAnimator` and `PhaseAnimator` runs every
|
|
741
|
-
frame while animating. Keep it to simple view modifiers.
|
|
742
|
-
|
|
743
|
-
```swift
|
|
744
|
-
// WRONG: Expensive computation per frame
|
|
745
|
-
.keyframeAnimator(initialValue: v, trigger: t) { content, value in
|
|
746
|
-
let result = heavyComputation(value.progress)
|
|
747
|
-
return content.opacity(result)
|
|
748
|
-
} keyframes: { _ in /* ... */ }
|
|
749
|
-
|
|
750
|
-
// CORRECT: Only apply view modifiers
|
|
751
|
-
.keyframeAnimator(initialValue: v, trigger: t) { content, value in
|
|
752
|
-
content.opacity(value.opacity)
|
|
753
|
-
} keyframes: { _ in /* ... */ }
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
### Prefer Modifier-Based Animations
|
|
757
|
-
|
|
758
|
-
Animating view modifiers (`opacity`, `scaleEffect`, `offset`, `rotationEffect`)
|
|
759
|
-
is highly optimized. Avoid animating layout-triggering properties when possible.
|
|
760
|
-
|
|
761
|
-
### Use drawingGroup for Complex Compositing
|
|
762
|
-
|
|
763
|
-
```swift
|
|
764
|
-
ComplexAnimatedView()
|
|
765
|
-
.drawingGroup()
|
|
766
|
-
```
|
|
767
|
-
|
|
768
|
-
Flattens the view hierarchy into a single Metal-backed layer. Use when
|
|
769
|
-
compositing many overlapping animated views.
|
|
770
|
-
|
|
771
|
-
### Limit Concurrent Animations
|
|
772
|
-
|
|
773
|
-
Avoid animating dozens of views simultaneously. Use staggered delays.
|
|
774
|
-
|
|
775
|
-
```swift
|
|
776
|
-
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
|
|
777
|
-
ItemView(item: item)
|
|
778
|
-
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
779
|
-
.animation(.spring.delay(Double(index) * 0.05), value: isVisible)
|
|
780
|
-
}
|
|
781
|
-
```
|
|
782
|
-
|
|
783
|
-
### Avoid Re-creating Views During Animation
|
|
784
|
-
|
|
785
|
-
Ensure animated views maintain stable identity. Use explicit `id()` modifiers
|
|
786
|
-
or stable `ForEach` identifiers.
|
|
787
|
-
|
|
788
|
-
```swift
|
|
789
|
-
// WRONG: View identity changes, breaks animation
|
|
790
|
-
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
|
|
791
|
-
ItemView(item: item)
|
|
792
|
-
}
|
|
495
|
+
Image(systemName: isLocked ? "lock.fill" : "lock.open.fill")
|
|
496
|
+
.contentTransition(.symbolEffect(.replace.downUp))
|
|
793
497
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
ItemView(item: item)
|
|
797
|
-
}
|
|
498
|
+
Image(systemName: isRecording ? "stop.circle.fill" : "record.circle")
|
|
499
|
+
.contentTransition(.symbolEffect(.replace.magic(fallback: .offUp))) // iOS 18
|
|
798
500
|
```
|
|
799
501
|
|
|
800
|
-
|
|
502
|
+
Directions: `.downUp`, `.offUp`, `.upUp`. `.magic` morphs shared parts of the
|
|
503
|
+
two symbols and falls back to the given direction when it cannot.
|
|
801
504
|
|
|
802
|
-
|
|
505
|
+
### Options
|
|
803
506
|
|
|
804
507
|
```swift
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
508
|
+
.symbolEffect(.bounce, options: .default, value: n)
|
|
509
|
+
.symbolEffect(.bounce, options: .repeating, value: n)
|
|
510
|
+
.symbolEffect(.bounce, options: .nonRepeating, value: n)
|
|
511
|
+
.symbolEffect(.bounce, options: .repeat(2), value: n)
|
|
512
|
+
.symbolEffect(.bounce, options: .speed(2.0), value: n)
|
|
513
|
+
.symbolEffect(.breathe, options: .repeat(.periodic(4, delay: 1.0)), isActive: on) // iOS 18
|
|
514
|
+
.symbolEffect(.breathe, options: .repeat(.continuous), isActive: on) // iOS 18
|
|
808
515
|
```
|
|
809
516
|
|
|
810
|
-
|
|
517
|
+
`.symbolEffectsRemoved(_:)` strips symbol effects from a subtree, for example
|
|
518
|
+
`.symbolEffectsRemoved(reduceMotion)`.
|
|
811
519
|
|
|
812
|
-
|
|
520
|
+
## Reduce Motion
|
|
813
521
|
|
|
814
522
|
```swift
|
|
815
|
-
|
|
816
|
-
ChildView()
|
|
817
|
-
.transaction { $0.animation = nil }
|
|
523
|
+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
818
524
|
```
|
|
819
525
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
526
|
+
1. **No animation.**
|
|
527
|
+
|
|
528
|
+
```swift
|
|
529
|
+
withAnimation(reduceMotion ? .none : .bouncy) { isOpen.toggle() }
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
2. **A calmer substitute.** Swap a spring or bounce for a short fade-like
|
|
533
|
+
curve:
|
|
534
|
+
|
|
535
|
+
```swift
|
|
536
|
+
.animation(reduceMotion ? .easeInOut(duration: 0.2) : .bouncy, value: isOpen)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
3. **No endless loops.** A `PhaseAnimator` without a trigger keeps moving no
|
|
540
|
+
matter what the setting says.
|
|
541
|
+
|
|
542
|
+
```swift
|
|
543
|
+
if reduceMotion {
|
|
544
|
+
LoadingDots()
|
|
545
|
+
} else {
|
|
546
|
+
PhaseAnimator([0.4, 1.0]) { level in
|
|
547
|
+
LoadingDots().opacity(level)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
The trigger form, which plays once per change, is the other option.
|
|
553
|
+
|
|
554
|
+
4. **Symbol effects.**
|
|
555
|
+
|
|
556
|
+
```swift
|
|
557
|
+
StatusIcons().symbolEffectsRemoved(reduceMotion)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
5. **A helper.**
|
|
561
|
+
|
|
562
|
+
```swift
|
|
563
|
+
extension Animation {
|
|
564
|
+
static func motionAware(_ preferred: Animation, reduceMotion: Bool) -> Animation? {
|
|
565
|
+
reduceMotion ? nil : preferred
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
withAnimation(.motionAware(.bouncy, reduceMotion: reduceMotion)) { isOpen.toggle() }
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## Performance
|
|
573
|
+
|
|
574
|
+
- **Per-frame closures stay light.** `KeyframeAnimator` and `PhaseAnimator`
|
|
575
|
+
call their content every frame.
|
|
576
|
+
|
|
577
|
+
```swift
|
|
578
|
+
// Wrong: sorts on every frame
|
|
579
|
+
.keyframeAnimator(initialValue: 0.0, trigger: t) { view, v in
|
|
580
|
+
view.overlay(Text(scores.sorted().first ?? "")).opacity(v)
|
|
581
|
+
} keyframes: { _ in LinearKeyframe(1.0, duration: 0.3) }
|
|
582
|
+
|
|
583
|
+
// Right: computed once, read per frame
|
|
584
|
+
let leader = scores.sorted().first ?? ""
|
|
585
|
+
// ... view.overlay(Text(leader)).opacity(v)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
- **Animate cheap properties.** `opacity`, `scaleEffect`, `offset` and
|
|
589
|
+
`rotationEffect` are handled efficiently by the renderer. Animating `frame`,
|
|
590
|
+
padding or anything else that forces layout costs more.
|
|
591
|
+
- **`.drawingGroup()`** flattens a subtree and draws it offscreen with Metal as
|
|
592
|
+
one image; useful when many overlapping views animate together.
|
|
593
|
+
- **Stagger large groups** instead of starting dozens of animations at once:
|
|
594
|
+
|
|
595
|
+
```swift
|
|
596
|
+
ForEach(Array(cards.enumerated()), id: \.element.id) { index, card in
|
|
597
|
+
CardView(card: card)
|
|
598
|
+
.offset(y: isShown ? 0 : 30)
|
|
599
|
+
.opacity(isShown ? 1 : 0)
|
|
600
|
+
.animation(.snappy.delay(Double(index) * 0.04), value: isShown)
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
- **Stable identity.** `ForEach(items.enumerated(), id: \.offset)` ties
|
|
605
|
+
identity to position, so inserts and deletes animate the wrong views. Use
|
|
606
|
+
the model's identity (`ForEach(items)`) or an explicit `.id()`.
|
|
607
|
+
- **`.geometryGroup()`** makes children resolve their geometry together, which
|
|
608
|
+
stops them fighting a parent's animation such as a `scaleEffect`.
|
|
609
|
+
- **Opt a child out** with `.transaction { $0.animation = nil }` while its
|
|
610
|
+
parent and siblings keep animating.
|
|
611
|
+
- **Measure** with the Core Animation instrument in Instruments: aim for
|
|
612
|
+
120 fps on ProMotion displays (60 elsewhere), no offscreen rendering passes,
|
|
613
|
+
and moderate GPU load.
|