@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
package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md
CHANGED
|
@@ -1,553 +1,504 @@
|
|
|
1
1
|
# Core Animation Bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Material that did not fit in the main SwiftUI animation skill. Reach for Core
|
|
4
|
+
Animation only when SwiftUI's animation system cannot do the job: layer animation on a
|
|
5
|
+
hot path, a curve SwiftUI cannot express, or direct work on a `CALayer`.
|
|
4
6
|
|
|
5
7
|
## Contents
|
|
6
8
|
|
|
7
|
-
- [When
|
|
9
|
+
- [When dropping down is justified](#when-dropping-down-is-justified)
|
|
10
|
+
- [The model value rule](#the-model-value-rule)
|
|
8
11
|
- [CABasicAnimation](#cabasicanimation)
|
|
9
12
|
- [CAKeyframeAnimation](#cakeyframeanimation)
|
|
10
13
|
- [CASpringAnimation](#caspringanimation)
|
|
11
14
|
- [CAAnimationGroup](#caanimationgroup)
|
|
12
15
|
- [CADisplayLink](#cadisplaylink)
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [Performance
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
- [Hosting layer animation in SwiftUI](#hosting-layer-animation-in-swiftui)
|
|
17
|
+
- [Moving state across the bridge](#moving-state-across-the-bridge)
|
|
18
|
+
- [Performance](#performance)
|
|
19
|
+
- [Apple documentation](#apple-documentation)
|
|
20
|
+
|
|
21
|
+
## When dropping down is justified
|
|
22
|
+
|
|
23
|
+
| Need | Why Core Animation |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Arbitrary timing | `CAMediaTimingFunction` takes any cubic Bezier control points |
|
|
26
|
+
| Layer-only properties | `shadowPath`, `borderWidth` and similar are not all reachable from SwiftUI |
|
|
27
|
+
| Additive animation | several animations can run on one property at the same time and blend |
|
|
28
|
+
| Drawing locked to the display | `CADisplayLink` fires once per refresh |
|
|
29
|
+
| Particles and ambient effects | plain layers skip SwiftUI's view diffing entirely |
|
|
30
|
+
| Motion along a path | `CAKeyframeAnimation` follows a `CGPath` |
|
|
31
|
+
|
|
32
|
+
If `withAnimation`, `PhaseAnimator` or `KeyframeAnimator` can do it, use them.
|
|
33
|
+
Every bridge means a `UIViewRepresentable`, layer lifetimes and state to keep
|
|
34
|
+
in sync.
|
|
35
|
+
|
|
36
|
+
## The model value rule
|
|
37
|
+
|
|
38
|
+
Core Animation animates the presentation layer, a copy of the layer drawn on
|
|
39
|
+
screen. The model layer keeps whatever value you last assigned. When the
|
|
40
|
+
animation is removed, the presentation snaps back to the model. So every time
|
|
41
|
+
you add an animation, also assign the end value to the layer itself.
|
|
42
|
+
|
|
43
|
+
`fillMode = .forwards` with `isRemovedOnCompletion = false` hides the snap by
|
|
44
|
+
keeping the finished animation attached, but the model still holds the old
|
|
45
|
+
value, hit testing and later animations start from the wrong place, and the
|
|
46
|
+
animation object is never released. Set the model value instead; keep that pair
|
|
47
|
+
for the rare case where the model value must not change.
|
|
31
48
|
|
|
32
49
|
## CABasicAnimation
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
### Basic Usage
|
|
51
|
+
One property, from one value to another:
|
|
37
52
|
|
|
38
53
|
```swift
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Apply to a layer
|
|
48
|
-
layer.add(animation, forKey: "fadeIn")
|
|
49
|
-
layer.opacity = 1.0 // Set the final model value
|
|
54
|
+
let fade = CABasicAnimation(keyPath: "opacity")
|
|
55
|
+
fade.fromValue = badgeLayer.opacity
|
|
56
|
+
fade.toValue = 0.2
|
|
57
|
+
fade.duration = 0.3
|
|
58
|
+
fade.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
59
|
+
badgeLayer.add(fade, forKey: "fade")
|
|
60
|
+
badgeLayer.opacity = 0.2 // model value
|
|
50
61
|
```
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
A custom Bezier curve on a position:
|
|
53
64
|
|
|
54
65
|
```swift
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
animation.timingFunction = timingFunction
|
|
63
|
-
animation.fillMode = .forwards
|
|
64
|
-
animation.isRemovedOnCompletion = false
|
|
65
|
-
|
|
66
|
-
layer.add(animation, forKey: "customBezier")
|
|
66
|
+
let rise = CABasicAnimation(keyPath: "position.y")
|
|
67
|
+
rise.fromValue = tagLayer.position.y
|
|
68
|
+
rise.toValue = tagLayer.position.y - 80
|
|
69
|
+
rise.duration = 0.5
|
|
70
|
+
rise.timingFunction = CAMediaTimingFunction(controlPoints: 0.16, 1.0, 0.3, 1.0)
|
|
71
|
+
tagLayer.add(rise, forKey: "rise")
|
|
72
|
+
tagLayer.position.y -= 80
|
|
67
73
|
```
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
Animating `shadowPath`, which SwiftUI has no handle on:
|
|
70
76
|
|
|
71
77
|
```swift
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
layer.shadowPath = UIBezierPath(roundedRect: newBounds, cornerRadius: 16).cgPath
|
|
80
|
-
layer.add(animation, forKey: "shadowPath")
|
|
78
|
+
let target = UIBezierPath(roundedRect: tileLayer.bounds, cornerRadius: 16).cgPath
|
|
79
|
+
let reshape = CABasicAnimation(keyPath: "shadowPath")
|
|
80
|
+
reshape.fromValue = tileLayer.shadowPath
|
|
81
|
+
reshape.toValue = target
|
|
82
|
+
reshape.duration = 0.35
|
|
83
|
+
tileLayer.add(reshape, forKey: "shadowPath")
|
|
84
|
+
tileLayer.shadowPath = target
|
|
81
85
|
```
|
|
82
86
|
|
|
83
|
-
**Important:** Always set the model value (the property on the layer itself) to the final state. Core Animation operates on a separate presentation layer -- without setting the model value, the layer snaps back when the animation completes.
|
|
84
|
-
|
|
85
|
-
> **Docs:** [CABasicAnimation](https://sosumi.ai/documentation/quartzcore/cabasicanimation) | [CAMediaTimingFunction](https://sosumi.ai/documentation/quartzcore/camediatimingfunction)
|
|
86
|
-
|
|
87
87
|
## CAKeyframeAnimation
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
### Value-Based Keyframes
|
|
89
|
+
### Through a list of values
|
|
92
90
|
|
|
93
91
|
```swift
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
CAMediaTimingFunction(name: .easeOut),
|
|
100
|
-
CAMediaTimingFunction(name: .easeIn),
|
|
92
|
+
let pop = CAKeyframeAnimation(keyPath: "transform.scale")
|
|
93
|
+
pop.values = [1.0, 1.25, 0.92, 1.04, 1.0]
|
|
94
|
+
pop.keyTimes = [0, 0.3, 0.55, 0.8, 1.0] // fractions of duration
|
|
95
|
+
pop.duration = 0.6
|
|
96
|
+
pop.timingFunctions = [
|
|
101
97
|
CAMediaTimingFunction(name: .easeOut),
|
|
102
|
-
CAMediaTimingFunction(name: .easeInEaseOut)
|
|
98
|
+
CAMediaTimingFunction(name: .easeInEaseOut),
|
|
99
|
+
CAMediaTimingFunction(name: .easeInEaseOut),
|
|
100
|
+
CAMediaTimingFunction(name: .easeIn)
|
|
103
101
|
]
|
|
104
|
-
|
|
105
|
-
layer.add(animation, forKey: "bounceScale")
|
|
102
|
+
iconLayer.add(pop, forKey: "pop")
|
|
106
103
|
```
|
|
107
104
|
|
|
108
|
-
###
|
|
105
|
+
### Along a path
|
|
109
106
|
|
|
110
107
|
```swift
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
animation.duration = 1.5
|
|
123
|
-
animation.rotationMode = .rotateAuto // Rotate along the tangent
|
|
124
|
-
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
125
|
-
|
|
126
|
-
layer.add(animation, forKey: "pathAnimation")
|
|
127
|
-
layer.position = CGPoint(x: 300, y: 50)
|
|
108
|
+
let route = CGMutablePath()
|
|
109
|
+
route.move(to: start)
|
|
110
|
+
route.addCurve(to: end, control1: CGPoint(x: start.x, y: end.y - 120),
|
|
111
|
+
control2: CGPoint(x: end.x, y: start.y - 120))
|
|
112
|
+
|
|
113
|
+
let fly = CAKeyframeAnimation(keyPath: "position")
|
|
114
|
+
fly.path = route
|
|
115
|
+
fly.rotationMode = .rotateAuto // face along the tangent
|
|
116
|
+
fly.duration = 0.9
|
|
117
|
+
planeLayer.add(fly, forKey: "fly")
|
|
118
|
+
planeLayer.position = end
|
|
128
119
|
```
|
|
129
120
|
|
|
130
|
-
###
|
|
121
|
+
### A shake helper
|
|
131
122
|
|
|
132
123
|
```swift
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
extension CALayer {
|
|
125
|
+
func shakeHorizontally() {
|
|
126
|
+
let shake = CAKeyframeAnimation(keyPath: "transform.translation.x")
|
|
127
|
+
shake.values = [0, -12, 12, -9, 9, -4, 4, 0]
|
|
128
|
+
shake.keyTimes = [0, 0.14, 0.28, 0.42, 0.57, 0.71, 0.85, 1]
|
|
129
|
+
shake.duration = 0.5
|
|
130
|
+
shake.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
131
|
+
add(shake, forKey: "shake")
|
|
132
|
+
}
|
|
140
133
|
}
|
|
141
134
|
```
|
|
142
135
|
|
|
143
|
-
|
|
136
|
+
The values return to zero, so no model value needs setting.
|
|
144
137
|
|
|
145
138
|
## CASpringAnimation
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
### Physical Spring Parameters
|
|
140
|
+
A `CABasicAnimation` subclass with `mass`, `stiffness`, `damping` and
|
|
141
|
+
`initialVelocity`:
|
|
150
142
|
|
|
151
143
|
```swift
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
layer.transform = CATransform3DIdentity
|
|
144
|
+
let settle = CASpringAnimation(keyPath: "transform.scale")
|
|
145
|
+
settle.mass = 1
|
|
146
|
+
settle.stiffness = 180
|
|
147
|
+
settle.damping = 14
|
|
148
|
+
settle.initialVelocity = 0
|
|
149
|
+
settle.fromValue = 0.85
|
|
150
|
+
settle.toValue = 1.0
|
|
151
|
+
settle.duration = settle.settlingDuration // let the spring finish
|
|
152
|
+
cardLayer.add(settle, forKey: "settle")
|
|
153
|
+
cardLayer.transform = CATransform3DIdentity
|
|
163
154
|
```
|
|
164
155
|
|
|
165
|
-
|
|
156
|
+
From iOS 17 the perceptual initializer matches SwiftUI's
|
|
157
|
+
`Spring(duration:bounce:)`:
|
|
166
158
|
|
|
167
159
|
```swift
|
|
168
|
-
let
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
layer.add(spring, forKey: "perceptualSpring")
|
|
174
|
-
layer.position.y -= 100
|
|
160
|
+
let settle = CASpringAnimation(perceptualDuration: 0.45, bounce: 0.2)
|
|
161
|
+
settle.keyPath = "transform.scale"
|
|
162
|
+
settle.fromValue = 0.85
|
|
163
|
+
settle.toValue = 1.0
|
|
175
164
|
```
|
|
176
165
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
### Matching SwiftUI Spring Presets
|
|
166
|
+
Rough equivalents of the SwiftUI presets. All three presets default to a
|
|
167
|
+
0.5 second duration and differ in bounce:
|
|
180
168
|
|
|
181
|
-
| SwiftUI
|
|
182
|
-
|
|
183
|
-
| `.smooth` |
|
|
184
|
-
| `.snappy` |
|
|
185
|
-
| `.bouncy` |
|
|
186
|
-
|
|
187
|
-
> **Docs:** [CASpringAnimation](https://sosumi.ai/documentation/quartzcore/caspringanimation)
|
|
169
|
+
| SwiftUI | `perceptualDuration` | `bounce` |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `.smooth` | 0.5 | 0.0 |
|
|
172
|
+
| `.snappy` | 0.5 | 0.15 |
|
|
173
|
+
| `.bouncy` | 0.5 | 0.3 |
|
|
188
174
|
|
|
189
175
|
## CAAnimationGroup
|
|
190
176
|
|
|
191
|
-
|
|
177
|
+
Several animations on one layer, sharing timing:
|
|
192
178
|
|
|
193
179
|
```swift
|
|
194
|
-
let
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
layer.opacity = 1.0
|
|
180
|
+
let grow = CABasicAnimation(keyPath: "transform.scale")
|
|
181
|
+
grow.fromValue = 0.6
|
|
182
|
+
grow.toValue = 1.0
|
|
183
|
+
|
|
184
|
+
let show = CABasicAnimation(keyPath: "opacity")
|
|
185
|
+
show.fromValue = 0
|
|
186
|
+
show.toValue = 1
|
|
187
|
+
|
|
188
|
+
let entrance = CAAnimationGroup()
|
|
189
|
+
entrance.animations = [grow, show]
|
|
190
|
+
entrance.duration = 0.4
|
|
191
|
+
entrance.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
192
|
+
bannerLayer.add(entrance, forKey: "entrance")
|
|
193
|
+
bannerLayer.transform = CATransform3DIdentity
|
|
194
|
+
bannerLayer.opacity = 1
|
|
210
195
|
```
|
|
211
196
|
|
|
212
|
-
> **Docs:** [CAAnimationGroup](https://sosumi.ai/documentation/quartzcore/caanimationgroup)
|
|
213
|
-
|
|
214
197
|
## CADisplayLink
|
|
215
198
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
### Basic Display Link
|
|
199
|
+
A timer that fires in step with the display.
|
|
219
200
|
|
|
220
201
|
```swift
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
private var displayLink: CADisplayLink?
|
|
202
|
+
@MainActor
|
|
203
|
+
final class CountUpDriver: NSObject {
|
|
204
|
+
private var link: CADisplayLink?
|
|
225
205
|
private var startTime: CFTimeInterval = 0
|
|
206
|
+
private let length: CFTimeInterval = 2
|
|
207
|
+
var onFrame: (Double) -> Void = { _ in }
|
|
226
208
|
|
|
227
209
|
func start() {
|
|
228
|
-
|
|
229
|
-
displayLink?.add(to: .main, forMode: .common)
|
|
210
|
+
guard link == nil else { return }
|
|
230
211
|
startTime = CACurrentMediaTime()
|
|
212
|
+
let link = CADisplayLink(target: self, selector: #selector(frame(_:)))
|
|
213
|
+
link.preferredFrameRateRange = CAFrameRateRange(minimum: 60, maximum: 120, preferred: 120)
|
|
214
|
+
link.add(to: .main, forMode: .common)
|
|
215
|
+
self.link = link
|
|
231
216
|
}
|
|
232
217
|
|
|
233
218
|
func stop() {
|
|
234
|
-
|
|
235
|
-
|
|
219
|
+
link?.invalidate()
|
|
220
|
+
link = nil
|
|
236
221
|
}
|
|
237
222
|
|
|
238
|
-
@objc private func
|
|
239
|
-
let
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Update rendering based on progress
|
|
243
|
-
updateAnimation(progress: progress)
|
|
244
|
-
|
|
245
|
-
if progress >= 1.0 {
|
|
246
|
-
stop()
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private func updateAnimation(progress: Double) {
|
|
251
|
-
// Custom per-frame rendering logic
|
|
223
|
+
@objc private func frame(_ link: CADisplayLink) {
|
|
224
|
+
let progress = min((link.timestamp - startTime) / length, 1)
|
|
225
|
+
onFrame(progress)
|
|
226
|
+
if progress >= 1 { stop() }
|
|
252
227
|
}
|
|
253
228
|
}
|
|
254
229
|
```
|
|
255
230
|
|
|
256
|
-
|
|
231
|
+
Frame-rate guidance for `preferredFrameRateRange` on ProMotion displays:
|
|
257
232
|
|
|
258
|
-
|
|
233
|
+
| Preferred | For |
|
|
234
|
+
|---|---|
|
|
235
|
+
| 120 | scrolling and following a finger |
|
|
236
|
+
| 60 | ordinary animation |
|
|
237
|
+
| 30 | slow ambient motion, or saving power |
|
|
259
238
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
minimum: 30,
|
|
263
|
-
maximum: 120,
|
|
264
|
-
preferred: 60
|
|
265
|
-
)
|
|
266
|
-
```
|
|
239
|
+
Always call `invalidate()` when finished. A display link left running keeps the
|
|
240
|
+
CPU awake and drains the battery, and it retains its target until invalidated.
|
|
267
241
|
|
|
268
|
-
|
|
269
|
-
|-------|----------|
|
|
270
|
-
| `preferred: 120` | Smooth scrolling, gesture tracking |
|
|
271
|
-
| `preferred: 60` | Standard animations |
|
|
272
|
-
| `preferred: 30` | Ambient/slow animations, power saving |
|
|
273
|
-
|
|
274
|
-
**Important:** Always call `invalidate()` when done. A running `CADisplayLink` prevents the CPU from idling and drains battery.
|
|
275
|
-
|
|
276
|
-
> **Docs:** [CADisplayLink](https://sosumi.ai/documentation/quartzcore/cadisplaylink) | [Optimizing ProMotion refresh rates](https://sosumi.ai/documentation/quartzcore/optimizing-promotion-refresh-rates-for-iphone-13-pro-and-ipad-pro)
|
|
277
|
-
|
|
278
|
-
## UIViewRepresentable Wrapper for CA Layers
|
|
279
|
-
|
|
280
|
-
To use Core Animation layers inside SwiftUI, wrap them in a `UIViewRepresentable`.
|
|
281
|
-
|
|
282
|
-
### Animated Layer View
|
|
242
|
+
## Hosting layer animation in SwiftUI
|
|
283
243
|
|
|
284
244
|
```swift
|
|
285
|
-
|
|
286
|
-
import QuartzCore
|
|
287
|
-
|
|
288
|
-
struct AnimatedLayerView: UIViewRepresentable {
|
|
245
|
+
struct SoundWave: UIViewRepresentable {
|
|
289
246
|
var isAnimating: Bool
|
|
290
|
-
var
|
|
247
|
+
var tint: UIColor
|
|
291
248
|
|
|
292
|
-
func makeUIView(context: Context) ->
|
|
293
|
-
|
|
294
|
-
return view
|
|
249
|
+
func makeUIView(context: Context) -> SoundWaveView {
|
|
250
|
+
SoundWaveView()
|
|
295
251
|
}
|
|
296
252
|
|
|
297
|
-
func updateUIView(_
|
|
298
|
-
|
|
299
|
-
|
|
253
|
+
func updateUIView(_ view: SoundWaveView, context: Context) {
|
|
254
|
+
view.setTint(tint)
|
|
300
255
|
if isAnimating {
|
|
301
|
-
|
|
256
|
+
view.startWaving()
|
|
302
257
|
} else {
|
|
303
|
-
|
|
258
|
+
view.stopWaving()
|
|
304
259
|
}
|
|
305
260
|
}
|
|
306
261
|
|
|
307
|
-
static func dismantleUIView(_
|
|
308
|
-
|
|
262
|
+
static func dismantleUIView(_ view: SoundWaveView, coordinator: ()) {
|
|
263
|
+
view.stopWaving()
|
|
309
264
|
}
|
|
310
265
|
}
|
|
311
|
-
```
|
|
312
266
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
final class AnimatedLayerUIView: UIView {
|
|
317
|
-
private let animationLayer = CAShapeLayer()
|
|
318
|
-
private var displayLink: CADisplayLink?
|
|
267
|
+
final class SoundWaveView: UIView {
|
|
268
|
+
private let waveLayer = CAShapeLayer()
|
|
269
|
+
private var link: CADisplayLink?
|
|
319
270
|
private var phase: CGFloat = 0
|
|
320
271
|
|
|
321
272
|
override init(frame: CGRect) {
|
|
322
273
|
super.init(frame: frame)
|
|
323
|
-
|
|
274
|
+
configureLayer()
|
|
324
275
|
}
|
|
325
276
|
|
|
326
277
|
required init?(coder: NSCoder) {
|
|
327
278
|
super.init(coder: coder)
|
|
328
|
-
|
|
279
|
+
configureLayer()
|
|
329
280
|
}
|
|
330
281
|
|
|
331
|
-
private func
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
layer.addSublayer(animationLayer)
|
|
282
|
+
private func configureLayer() {
|
|
283
|
+
waveLayer.fillColor = UIColor.systemBlue.cgColor
|
|
284
|
+
layer.addSublayer(waveLayer)
|
|
335
285
|
}
|
|
336
286
|
|
|
337
287
|
override func layoutSubviews() {
|
|
338
288
|
super.layoutSubviews()
|
|
339
|
-
|
|
340
|
-
updatePath()
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
func updateColor(_ color: UIColor) {
|
|
344
|
-
// Animate color change at the CA layer level
|
|
345
|
-
let animation = CABasicAnimation(keyPath: "fillColor")
|
|
346
|
-
animation.fromValue = animationLayer.fillColor
|
|
347
|
-
animation.toValue = color.cgColor
|
|
348
|
-
animation.duration = 0.3
|
|
349
|
-
|
|
350
|
-
animationLayer.fillColor = color.cgColor
|
|
351
|
-
animationLayer.add(animation, forKey: "colorChange")
|
|
289
|
+
waveLayer.frame = bounds
|
|
352
290
|
}
|
|
353
291
|
|
|
354
|
-
func
|
|
355
|
-
guard
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
292
|
+
func setTint(_ color: UIColor) {
|
|
293
|
+
guard waveLayer.fillColor != color.cgColor else { return }
|
|
294
|
+
let recolor = CABasicAnimation(keyPath: "fillColor")
|
|
295
|
+
recolor.fromValue = waveLayer.fillColor
|
|
296
|
+
recolor.toValue = color.cgColor
|
|
297
|
+
recolor.duration = 0.25
|
|
298
|
+
waveLayer.add(recolor, forKey: "fillColor")
|
|
299
|
+
waveLayer.fillColor = color.cgColor
|
|
361
300
|
}
|
|
362
301
|
|
|
363
|
-
func
|
|
364
|
-
|
|
365
|
-
|
|
302
|
+
func startWaving() {
|
|
303
|
+
guard link == nil else { return }
|
|
304
|
+
let link = CADisplayLink(target: self, selector: #selector(advanceWave(_:)))
|
|
305
|
+
link.preferredFrameRateRange = CAFrameRateRange(minimum: 20, maximum: 60, preferred: 30)
|
|
306
|
+
link.add(to: .main, forMode: .common)
|
|
307
|
+
self.link = link
|
|
366
308
|
}
|
|
367
309
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
310
|
+
func stopWaving() {
|
|
311
|
+
link?.invalidate()
|
|
312
|
+
link = nil
|
|
371
313
|
}
|
|
372
314
|
|
|
373
|
-
private func
|
|
374
|
-
|
|
375
|
-
let
|
|
376
|
-
|
|
377
|
-
let midY = height / 2
|
|
378
|
-
|
|
379
|
-
path.move(to: CGPoint(x: 0, y: midY))
|
|
380
|
-
for x in stride(from: 0, to: width, by: 2) {
|
|
381
|
-
let relativeX = x / width
|
|
382
|
-
let y = midY + sin((relativeX * .pi * 4) + phase) * (height * 0.3)
|
|
383
|
-
path.addLine(to: CGPoint(x: x, y: y))
|
|
315
|
+
@objc private func advanceWave(_ link: CADisplayLink) {
|
|
316
|
+
phase += 0.12
|
|
317
|
+
let crest = stride(from: bounds.minX, through: bounds.maxX, by: 3).map { x in
|
|
318
|
+
CGPoint(x: x, y: bounds.midY + sin(x / 24 + phase) * 18)
|
|
384
319
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
320
|
+
let outline = CGMutablePath()
|
|
321
|
+
outline.addLines(between: [CGPoint(x: bounds.minX, y: bounds.maxY)]
|
|
322
|
+
+ crest + [CGPoint(x: bounds.maxX, y: bounds.maxY)])
|
|
323
|
+
outline.closeSubpath()
|
|
324
|
+
waveLayer.path = outline
|
|
390
325
|
}
|
|
391
326
|
}
|
|
392
327
|
```
|
|
393
328
|
|
|
394
|
-
|
|
329
|
+
In SwiftUI:
|
|
395
330
|
|
|
396
331
|
```swift
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
.frame(height: 200)
|
|
404
|
-
.clipShape(.rect(cornerRadius: 16))
|
|
405
|
-
}
|
|
406
|
-
.buttonStyle(.plain)
|
|
407
|
-
}
|
|
332
|
+
@State private var recording = false
|
|
333
|
+
|
|
334
|
+
Button { recording.toggle() } label: {
|
|
335
|
+
SoundWave(isAnimating: recording, tint: recording ? .systemRed : .systemBlue)
|
|
336
|
+
.frame(height: 200)
|
|
337
|
+
.clipShape(.rect(cornerRadius: 16))
|
|
408
338
|
}
|
|
409
339
|
```
|
|
410
340
|
|
|
411
|
-
|
|
341
|
+
Rules for the wrapper:
|
|
412
342
|
|
|
413
|
-
1.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
343
|
+
1. Layers are built once, when the view is made (`makeUIView` or the
|
|
344
|
+
`UIView` initializer). `updateUIView` only changes them.
|
|
345
|
+
2. `dismantleUIView` is where display links get invalidated; skip it and they
|
|
346
|
+
leak and keep the CPU busy after the view is gone.
|
|
347
|
+
3. SwiftUI calls `updateUIView` for any relevant state change, so a start call
|
|
348
|
+
there must be idempotent.
|
|
349
|
+
4. Assign model values next to every Core Animation animation.
|
|
417
350
|
|
|
418
|
-
##
|
|
351
|
+
## Moving state across the bridge
|
|
419
352
|
|
|
420
|
-
###
|
|
353
|
+
### Firing an animation from SwiftUI
|
|
421
354
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
var pulseCount: Int // Increment to trigger a pulse
|
|
355
|
+
Pass a counter in. The coordinator remembers the last value, so the view
|
|
356
|
+
animates only when the counter actually changes:
|
|
425
357
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
358
|
+
```swift
|
|
359
|
+
struct Ripple: UIViewRepresentable {
|
|
360
|
+
var fireCount: Int
|
|
429
361
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if context.coordinator.lastPulseCount != pulseCount {
|
|
433
|
-
context.coordinator.lastPulseCount = pulseCount
|
|
434
|
-
uiView.pulse()
|
|
435
|
-
}
|
|
362
|
+
final class Coordinator {
|
|
363
|
+
var lastFired = 0
|
|
436
364
|
}
|
|
437
365
|
|
|
438
366
|
func makeCoordinator() -> Coordinator { Coordinator() }
|
|
439
367
|
|
|
440
|
-
|
|
441
|
-
|
|
368
|
+
func makeUIView(context: Context) -> RippleView { RippleView() }
|
|
369
|
+
|
|
370
|
+
func updateUIView(_ view: RippleView, context: Context) {
|
|
371
|
+
guard fireCount != context.coordinator.lastFired else { return }
|
|
372
|
+
context.coordinator.lastFired = fireCount
|
|
373
|
+
view.emit()
|
|
442
374
|
}
|
|
443
375
|
}
|
|
444
376
|
|
|
445
|
-
final class
|
|
446
|
-
private let
|
|
377
|
+
final class RippleView: UIView {
|
|
378
|
+
private let ring = CAShapeLayer()
|
|
447
379
|
|
|
448
380
|
override init(frame: CGRect) {
|
|
449
381
|
super.init(frame: frame)
|
|
450
|
-
|
|
451
|
-
|
|
382
|
+
ring.fillColor = UIColor.systemTeal.cgColor
|
|
383
|
+
ring.opacity = 0
|
|
384
|
+
layer.addSublayer(ring)
|
|
452
385
|
}
|
|
453
386
|
|
|
454
|
-
required init?(coder: NSCoder) { fatalError() }
|
|
387
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
|
455
388
|
|
|
456
389
|
override func layoutSubviews() {
|
|
457
390
|
super.layoutSubviews()
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
x: (bounds.width - size) / 2,
|
|
461
|
-
y: (bounds.height - size) / 2,
|
|
462
|
-
width: size,
|
|
463
|
-
height: size
|
|
464
|
-
)
|
|
465
|
-
pulseLayer.path = UIBezierPath(ovalIn: rect).cgPath
|
|
391
|
+
ring.frame = bounds
|
|
392
|
+
ring.path = UIBezierPath(ovalIn: bounds.insetBy(dx: bounds.width / 4, dy: bounds.height / 4)).cgPath
|
|
466
393
|
}
|
|
467
394
|
|
|
468
|
-
func
|
|
469
|
-
let
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
pulseLayer.add(group, forKey: "pulse")
|
|
395
|
+
func emit() {
|
|
396
|
+
let spread = CABasicAnimation(keyPath: "transform.scale")
|
|
397
|
+
spread.fromValue = 1.0
|
|
398
|
+
spread.toValue = 1.5
|
|
399
|
+
let fade = CABasicAnimation(keyPath: "opacity")
|
|
400
|
+
fade.fromValue = 1.0
|
|
401
|
+
fade.toValue = 0.0
|
|
402
|
+
|
|
403
|
+
let burst = CAAnimationGroup()
|
|
404
|
+
burst.animations = [spread, fade]
|
|
405
|
+
burst.duration = 0.6
|
|
406
|
+
burst.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
407
|
+
ring.add(burst, forKey: "burst")
|
|
408
|
+
ring.opacity = 0
|
|
483
409
|
}
|
|
484
410
|
}
|
|
485
411
|
```
|
|
486
412
|
|
|
487
|
-
###
|
|
413
|
+
### Reporting completion back to SwiftUI
|
|
488
414
|
|
|
489
|
-
|
|
415
|
+
The coordinator is the animation's delegate. It owns the link back to SwiftUI,
|
|
416
|
+
so `animationDidStop` can write through the `Binding`:
|
|
490
417
|
|
|
491
418
|
```swift
|
|
492
|
-
struct
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
|
419
|
+
struct SpringyBadge: UIViewRepresentable {
|
|
420
|
+
var trigger: Int
|
|
421
|
+
@Binding var settled: Bool
|
|
496
422
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
badge.fillColor = UIColor.systemRed.cgColor
|
|
502
|
-
badge.name = "badge"
|
|
503
|
-
view.layer.addSublayer(badge)
|
|
504
|
-
return view
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
func updateUIView(_ uiView: UIView, context: Context) {}
|
|
508
|
-
|
|
509
|
-
func animateIn(_ uiView: UIView) {
|
|
510
|
-
guard let badge = uiView.layer.sublayers?.first(where: { $0.name == "badge" }) else { return }
|
|
423
|
+
@MainActor
|
|
424
|
+
final class Coordinator: NSObject, CAAnimationDelegate {
|
|
425
|
+
var parent: SpringyBadge
|
|
426
|
+
var lastTrigger = 0
|
|
511
427
|
|
|
512
|
-
|
|
513
|
-
spring.keyPath = "transform.scale"
|
|
514
|
-
spring.fromValue = 0.0
|
|
515
|
-
spring.toValue = 1.0
|
|
516
|
-
spring.delegate = uiView.next as? CAAnimationDelegate
|
|
428
|
+
init(owner: SpringyBadge) { parent = owner }
|
|
517
429
|
|
|
518
|
-
|
|
519
|
-
|
|
430
|
+
nonisolated func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
|
|
431
|
+
guard flag else { return }
|
|
432
|
+
MainActor.assumeIsolated { parent.settled = true }
|
|
433
|
+
}
|
|
520
434
|
}
|
|
521
435
|
|
|
522
|
-
|
|
523
|
-
var parent: AnimatedBadge
|
|
436
|
+
func makeCoordinator() -> Coordinator { Coordinator(owner: self) }
|
|
524
437
|
|
|
525
|
-
|
|
438
|
+
func makeUIView(context: Context) -> UIView {
|
|
439
|
+
let host = UIView()
|
|
440
|
+
let dot = CAShapeLayer()
|
|
441
|
+
dot.name = "dot"
|
|
442
|
+
dot.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 40, height: 40)), transform: nil)
|
|
443
|
+
dot.fillColor = UIColor.systemOrange.cgColor
|
|
444
|
+
host.layer.addSublayer(dot)
|
|
445
|
+
return host
|
|
446
|
+
}
|
|
526
447
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
448
|
+
func updateUIView(_ host: UIView, context: Context) {
|
|
449
|
+
context.coordinator.parent = self // keep the binding current
|
|
450
|
+
guard trigger != context.coordinator.lastTrigger,
|
|
451
|
+
let dot = host.layer.sublayers?.first(where: { $0.name == "dot" })
|
|
452
|
+
else { return }
|
|
453
|
+
context.coordinator.lastTrigger = trigger
|
|
454
|
+
|
|
455
|
+
let grow = CASpringAnimation(perceptualDuration: 0.4, bounce: 0.35)
|
|
456
|
+
grow.keyPath = "transform.scale"
|
|
457
|
+
grow.fromValue = 0.6
|
|
458
|
+
grow.toValue = 1.0
|
|
459
|
+
grow.delegate = context.coordinator
|
|
460
|
+
dot.add(grow, forKey: "grow")
|
|
461
|
+
dot.transform = CATransform3DIdentity
|
|
532
462
|
}
|
|
533
463
|
}
|
|
534
464
|
```
|
|
535
465
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
466
|
+
The delegate must be `context.coordinator`. A plain `UIView` does not conform
|
|
467
|
+
to `CAAnimationDelegate`, so looking one up through the responder chain gives
|
|
468
|
+
`nil` and the callback never fires. Refreshing `coordinator.parent` in
|
|
469
|
+
`updateUIView` keeps the binding pointing at the current state.
|
|
470
|
+
|
|
471
|
+
Reset `settled` to `false` in the SwiftUI code that increments `trigger`, not
|
|
472
|
+
inside `updateUIView`; writing state during a view update triggers a runtime
|
|
473
|
+
warning and an extra render pass.
|
|
474
|
+
|
|
475
|
+
## Performance
|
|
476
|
+
|
|
477
|
+
| | SwiftUI animation | Core Animation |
|
|
478
|
+
|---|---|---|
|
|
479
|
+
| Mechanism | view diffing and the render tree | direct layer changes |
|
|
480
|
+
| Per frame | `body` of animatable views re-evaluated as data interpolates | no `body` evaluation |
|
|
481
|
+
| Best for | ordinary UI transitions | particles, waves, complex paths |
|
|
482
|
+
|
|
483
|
+
Both end up composited by the render server, and both keep their state on the
|
|
484
|
+
main thread.
|
|
485
|
+
|
|
486
|
+
- Never animate one property from both systems. They do not coordinate and
|
|
487
|
+
will fight.
|
|
488
|
+
- Use `CADisplayLink` sparingly and invalidate it the moment it is not needed.
|
|
489
|
+
- For animated paths, update a `CAShapeLayer` rather than redrawing in
|
|
490
|
+
`draw(_:)`; shape layers are rendered on the GPU.
|
|
491
|
+
- `shouldRasterize = true` caches a complex, static sublayer tree as a bitmap.
|
|
492
|
+
Turn it off while that tree animates, or the cache is rebuilt every frame.
|
|
493
|
+
- Build Core Animation springs with `perceptualDuration:bounce:` so they feel
|
|
494
|
+
like the SwiftUI springs next to them.
|
|
495
|
+
|
|
496
|
+
## Apple documentation
|
|
497
|
+
|
|
498
|
+
- [CABasicAnimation](https://developer.apple.com/documentation/quartzcore/cabasicanimation)
|
|
499
|
+
- [CAMediaTimingFunction](https://developer.apple.com/documentation/quartzcore/camediatimingfunction)
|
|
500
|
+
- [CAKeyframeAnimation](https://developer.apple.com/documentation/quartzcore/cakeyframeanimation)
|
|
501
|
+
- [CASpringAnimation](https://developer.apple.com/documentation/quartzcore/caspringanimation)
|
|
502
|
+
- [CAAnimationGroup](https://developer.apple.com/documentation/quartzcore/caanimationgroup)
|
|
503
|
+
- [CADisplayLink](https://developer.apple.com/documentation/quartzcore/cadisplaylink)
|
|
504
|
+
- [Optimizing ProMotion refresh rates](https://developer.apple.com/documentation/quartzcore/optimizing-promotion-refresh-rates-for-iphone-13-pro-and-ipad-pro)
|