@mmerterden/multi-agent-pipeline 20.6.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 +27 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +328 -329
- package/package.json +2 -2
- package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
- package/pipeline/scripts/_notices.mjs +12 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +106 -106
- package/pipeline/skills/shared/README.md +71 -71
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
- 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/android-architecture/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
- 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/backlog/BACKLOG.md +1 -1
- package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
- 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/ci-cd-pipelines/SKILL.md +1 -0
- 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/compose-components/SKILL.md +2 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
- 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/council/SKILL.md +1 -0
- 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/css-modern/SKILL.md +1 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
- 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/evidence-github/SKILL.md +2 -0
- package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
- 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/html-semantic/SKILL.md +1 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
- 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-coding-standard/SKILL.md +1 -0
- 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-module-structure/SKILL.md +1 -0
- 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-security/SKILL.md +2 -0
- 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 +91 -283
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
- 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/nextjs-app-router/SKILL.md +1 -0
- 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/play-store-review/SKILL.md +2 -0
- 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/python-patterns/SKILL.md +1 -0
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
- 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/rest-api-design/SKILL.md +1 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
- package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
- 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/signal-community/SKILL.md +2 -0
- package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
- package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
- package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
- package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
- package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
- package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
- package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
- 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/tailwind-css/SKILL.md +1 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
- 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/typescript-patterns/SKILL.md +1 -0
- 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/vue-composition/SKILL.md +1 -0
- 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/web-accessibility/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,1242 +1,935 @@
|
|
|
1
|
-
# Deprecated API Migration
|
|
1
|
+
# Deprecated API Migration
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A map from older SwiftUI and iOS patterns (iOS 15 era through iOS 26) to their
|
|
4
|
+
current replacements. Each entry shows the old form, the new form, and notes.
|
|
5
|
+
Code is written for iOS 26 and Swift 6.3. Every entry names the release its
|
|
6
|
+
replacement needs, so check that against the project's deployment target
|
|
7
|
+
before migrating; nothing here assumes a single baseline.
|
|
8
|
+
|
|
9
|
+
Several old APIs here (`NavigationView`, `foregroundColor`, `cornerRadius`,
|
|
10
|
+
`ActionSheet`, the `Alert` struct, `presentationMode`, `tabItem`, the
|
|
11
|
+
`.navigationBar` toolbar placements) carry `deprecated: 100000.0` in the SDK.
|
|
12
|
+
Apple's documentation lists them as deprecated, but the compiler does not warn,
|
|
13
|
+
so a clean build does not mean they are gone; search for them.
|
|
4
14
|
|
|
5
15
|
## Contents
|
|
6
|
-
- NavigationView to NavigationStack
|
|
7
|
-
- NavigationView Sidebar to NavigationSplitView
|
|
8
|
-
- ObservableObject / `@Published` / `@StateObject` to `@Observable` / `@State`
|
|
9
|
-
- `@ObservedObject` to let / `@Bindable`
|
|
10
|
-
- `@EnvironmentObject` to `@Environment`
|
|
11
|
-
- foregroundColor to foregroundStyle
|
|
12
|
-
- .onChange single-value to two-value
|
|
13
|
-
- ActionSheet to confirmationDialog
|
|
14
|
-
- Alert (Legacy) to modern .alert
|
|
15
|
-
- AnyView to `@ViewBuilder`
|
|
16
|
-
- .onAppear + Task to .task
|
|
17
|
-
- presentationMode to dismiss
|
|
18
|
-
- GeometryReader to Layout / containerRelativeFrame
|
|
19
|
-
- PreviewProvider to #Preview
|
|
20
|
-
- XCTest to Swift Testing
|
|
21
|
-
- EditButton/.onDelete to .swipeActions
|
|
22
|
-
- UIApplication.shared.open to openURL
|
|
23
|
-
- `@FetchRequest` to @Query (SwiftData)
|
|
24
|
-
- some vs any return types
|
|
25
|
-
- .sheet(item:) for sheet presentation
|
|
26
|
-
- Color.resolve(in:) usage
|
|
27
|
-
- ForEach with Identifiable
|
|
28
|
-
- Toolbar placement updates
|
|
29
|
-
|
|
30
|
-
## NavigationView to NavigationStack
|
|
31
|
-
|
|
32
|
-
NavigationView was deprecated in iOS 16. Use NavigationStack for push-based navigation with a single column, or NavigationSplitView for multi-column layouts.
|
|
33
|
-
|
|
34
|
-
### Before (Deprecated)
|
|
35
16
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
17
|
+
1. [NavigationView to NavigationStack](#1-navigationview-to-navigationstack)
|
|
18
|
+
2. [NavigationView Sidebar to NavigationSplitView](#2-navigationview-sidebar-to-navigationsplitview)
|
|
19
|
+
3. [ObservableObject and @StateObject to @Observable and @State](#3-observableobject-and-stateobject-to-observable-and-state)
|
|
20
|
+
4. [@ObservedObject to let or @Bindable](#4-observedobject-to-let-or-bindable)
|
|
21
|
+
5. [@EnvironmentObject to @Environment](#5-environmentobject-to-environment)
|
|
22
|
+
6. [foregroundColor to foregroundStyle](#6-foregroundcolor-to-foregroundstyle)
|
|
23
|
+
7. [Single-Value onChange to Two-Value onChange](#7-single-value-onchange-to-two-value-onchange)
|
|
24
|
+
8. [ActionSheet to confirmationDialog](#8-actionsheet-to-confirmationdialog)
|
|
25
|
+
9. [Legacy Alert to alert with Actions](#9-legacy-alert-to-alert-with-actions)
|
|
26
|
+
10. [AnyView to @ViewBuilder and Concrete Types](#10-anyview-to-viewbuilder-and-concrete-types)
|
|
27
|
+
11. [onAppear with Task to .task](#11-onappear-with-task-to-task)
|
|
28
|
+
12. [presentationMode to dismiss](#12-presentationmode-to-dismiss)
|
|
29
|
+
13. [GeometryReader Overuse to containerRelativeFrame and Layout](#13-geometryreader-overuse-to-containerrelativeframe-and-layout)
|
|
30
|
+
14. [PreviewProvider to the #Preview Macro](#14-previewprovider-to-the-preview-macro)
|
|
31
|
+
15. [XCTest to Swift Testing](#15-xctest-to-swift-testing)
|
|
32
|
+
16. [onDelete and EditButton to swipeActions](#16-ondelete-and-editbutton-to-swipeactions)
|
|
33
|
+
17. [UIApplication.shared.open to openURL](#17-uiapplicationsharedopen-to-openurl)
|
|
34
|
+
18. [@FetchRequest to @Query](#18-fetchrequest-to-query)
|
|
35
|
+
19. [Type-Erased Returns to some and any](#19-type-erased-returns-to-some-and-any)
|
|
36
|
+
20. [sheet(isPresented:) with an Optional to sheet(item:)](#20-sheetispresented-with-an-optional-to-sheetitem)
|
|
37
|
+
21. [UIColor Interop to Asset Colors and Color.resolve](#21-uicolor-interop-to-asset-colors-and-colorresolve)
|
|
38
|
+
22. [ForEach over a Range to Identifiable Data](#22-foreach-over-a-range-to-identifiable-data)
|
|
39
|
+
23. [Toolbar Placements navigationBar to topBar](#23-toolbar-placements-navigationbar-to-topbar)
|
|
40
|
+
24. [cornerRadius to clipShape](#24-cornerradius-to-clipshape)
|
|
41
|
+
25. [tabItem to Tab (iOS 18)](#25-tabitem-to-tab-ios-18)
|
|
42
|
+
26. [showsIndicators to scrollIndicators](#26-showsindicators-to-scrollindicators)
|
|
43
|
+
|
|
44
|
+
## 1. NavigationView to NavigationStack
|
|
45
|
+
|
|
46
|
+
`NavigationView` has been superseded since iOS 16 and is marked for deprecation
|
|
47
|
+
(no compiler warning). Use `NavigationStack` (iOS 16+)
|
|
48
|
+
for one column of pushed screens and `NavigationSplitView` for several columns.
|
|
49
|
+
|
|
50
|
+
Before:
|
|
51
|
+
|
|
52
|
+
```swift
|
|
53
|
+
NavigationView {
|
|
54
|
+
List(courses) { course in
|
|
55
|
+
NavigationLink(destination: CourseDetail(course: course)) {
|
|
56
|
+
Text(course.title)
|
|
46
57
|
}
|
|
47
|
-
.navigationViewStyle(.stack)
|
|
48
58
|
}
|
|
49
59
|
}
|
|
60
|
+
.navigationViewStyle(.stack)
|
|
50
61
|
```
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
After:
|
|
53
64
|
|
|
54
65
|
```swift
|
|
55
|
-
|
|
56
|
-
@State private var path: [Item] = []
|
|
66
|
+
@State private var path: [Course] = []
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
NavigationLink(value: item) {
|
|
62
|
-
Text(item.title)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
.navigationTitle("Items")
|
|
66
|
-
.navigationDestination(for: Item.self) { item in
|
|
67
|
-
DetailView(item: item)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
68
|
+
NavigationStack(path: $path) {
|
|
69
|
+
List(courses) { course in
|
|
70
|
+
NavigationLink(course.title, value: course)
|
|
70
71
|
}
|
|
72
|
+
.navigationDestination(for: Course.self) { CourseDetail(course: $0) }
|
|
71
73
|
}
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
---
|
|
76
|
+
- The path binding gives code full control over what is pushed.
|
|
77
|
+
- A value-based `NavigationLink` separates the trigger from the destination, so
|
|
78
|
+
rows stay cheap.
|
|
79
|
+
- `.navigationViewStyle(.stack)` is no longer needed.
|
|
79
80
|
|
|
80
|
-
## NavigationView Sidebar to NavigationSplitView
|
|
81
|
+
## 2. NavigationView Sidebar to NavigationSplitView
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
Before:
|
|
83
84
|
|
|
84
85
|
```swift
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
SidebarList()
|
|
89
|
-
DetailPlaceholder()
|
|
90
|
-
}
|
|
91
|
-
.navigationViewStyle(.columns)
|
|
92
|
-
}
|
|
86
|
+
NavigationView {
|
|
87
|
+
TeamSidebar()
|
|
88
|
+
Text("Choose a team")
|
|
93
89
|
}
|
|
90
|
+
.navigationViewStyle(.columns)
|
|
94
91
|
```
|
|
95
92
|
|
|
96
|
-
|
|
93
|
+
After (iOS 16+):
|
|
97
94
|
|
|
98
95
|
```swift
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@State private var selectedItem: Item?
|
|
96
|
+
@State private var team: Team?
|
|
97
|
+
@State private var player: Player?
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
systemImage: "sidebar.left")
|
|
117
|
-
}
|
|
118
|
-
} detail: {
|
|
119
|
-
if let item = selectedItem {
|
|
120
|
-
DetailView(item: item)
|
|
121
|
-
} else {
|
|
122
|
-
ContentUnavailableView("Select an Item",
|
|
123
|
-
systemImage: "doc.text")
|
|
124
|
-
}
|
|
125
|
-
}
|
|
99
|
+
NavigationSplitView {
|
|
100
|
+
List(teams, selection: $team) { NavigationLink($0.name, value: $0) }
|
|
101
|
+
} content: {
|
|
102
|
+
if let team {
|
|
103
|
+
List(team.players, selection: $player) { NavigationLink($0.name, value: $0) }
|
|
104
|
+
} else {
|
|
105
|
+
ContentUnavailableView("No team selected", systemImage: "sidebar.left")
|
|
106
|
+
}
|
|
107
|
+
} detail: {
|
|
108
|
+
if let player {
|
|
109
|
+
PlayerCard(player: player)
|
|
110
|
+
} else {
|
|
111
|
+
ContentUnavailableView("No player selected", systemImage: "doc.text")
|
|
126
112
|
}
|
|
127
113
|
}
|
|
128
114
|
```
|
|
129
115
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
116
|
+
- The split view states its two- or three-column layout explicitly.
|
|
117
|
+
- Column visibility is a `NavigationSplitViewVisibility` value passed through
|
|
118
|
+
the `columnVisibility:` binding.
|
|
119
|
+
- In compact size classes it collapses into a stack on its own.
|
|
133
120
|
|
|
134
|
-
|
|
121
|
+
## 3. ObservableObject and @StateObject to @Observable and @State
|
|
135
122
|
|
|
136
|
-
|
|
123
|
+
The Observation framework (iOS 17+) replaces observation built on Combine. An
|
|
124
|
+
`@Observable` class records which properties each view reads.
|
|
137
125
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
### Before (Superseded)
|
|
126
|
+
Before:
|
|
141
127
|
|
|
142
128
|
```swift
|
|
143
|
-
class
|
|
144
|
-
@Published var
|
|
145
|
-
@Published var
|
|
146
|
-
@Published var
|
|
147
|
-
|
|
148
|
-
func
|
|
149
|
-
username = ""
|
|
150
|
-
notificationsEnabled = true
|
|
151
|
-
theme = .system
|
|
152
|
-
}
|
|
129
|
+
final class ReaderSettings: ObservableObject {
|
|
130
|
+
@Published var fontSize = 16.0
|
|
131
|
+
@Published var isNightMode = false
|
|
132
|
+
@Published var typeface = "Serif"
|
|
133
|
+
|
|
134
|
+
func restoreDefaults() { fontSize = 16; isNightMode = false; typeface = "Serif" }
|
|
153
135
|
}
|
|
154
136
|
|
|
155
|
-
struct
|
|
156
|
-
@StateObject private var settings =
|
|
137
|
+
struct ReaderSettingsView: View {
|
|
138
|
+
@StateObject private var settings = ReaderSettings()
|
|
157
139
|
|
|
158
140
|
var body: some View {
|
|
159
141
|
Form {
|
|
160
|
-
|
|
161
|
-
Toggle("
|
|
162
|
-
Picker("
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
142
|
+
Slider(value: $settings.fontSize, in: 12...32)
|
|
143
|
+
Toggle("Night mode", isOn: $settings.isNightMode)
|
|
144
|
+
Picker("Typeface", selection: $settings.typeface) {
|
|
145
|
+
Text("Serif").tag("Serif")
|
|
146
|
+
Text("Sans").tag("Sans")
|
|
166
147
|
}
|
|
167
148
|
}
|
|
168
149
|
}
|
|
169
150
|
}
|
|
170
151
|
```
|
|
171
152
|
|
|
172
|
-
|
|
153
|
+
After:
|
|
173
154
|
|
|
174
155
|
```swift
|
|
175
|
-
@Observable
|
|
176
|
-
class
|
|
177
|
-
var
|
|
178
|
-
var
|
|
179
|
-
var
|
|
180
|
-
|
|
181
|
-
func
|
|
182
|
-
username = ""
|
|
183
|
-
notificationsEnabled = true
|
|
184
|
-
theme = .system
|
|
185
|
-
}
|
|
156
|
+
@MainActor @Observable
|
|
157
|
+
final class ReaderSettings {
|
|
158
|
+
var fontSize = 16.0
|
|
159
|
+
var isNightMode = false
|
|
160
|
+
var typeface = "Serif"
|
|
161
|
+
|
|
162
|
+
func restoreDefaults() { fontSize = 16; isNightMode = false; typeface = "Serif" }
|
|
186
163
|
}
|
|
187
164
|
|
|
188
|
-
struct
|
|
189
|
-
@State private var settings =
|
|
165
|
+
struct ReaderSettingsView: View {
|
|
166
|
+
@State private var settings = ReaderSettings()
|
|
190
167
|
|
|
191
168
|
var body: some View {
|
|
192
169
|
Form {
|
|
193
|
-
|
|
194
|
-
Toggle("
|
|
195
|
-
Picker("
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
170
|
+
Slider(value: $settings.fontSize, in: 12...32)
|
|
171
|
+
Toggle("Night mode", isOn: $settings.isNightMode)
|
|
172
|
+
Picker("Typeface", selection: $settings.typeface) {
|
|
173
|
+
Text("Serif").tag("Serif")
|
|
174
|
+
Text("Sans").tag("Sans")
|
|
199
175
|
}
|
|
200
176
|
}
|
|
201
177
|
}
|
|
202
178
|
}
|
|
203
179
|
```
|
|
204
180
|
|
|
205
|
-
|
|
181
|
+
Steps:
|
|
206
182
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- The view only re-evaluates when properties it actually reads change, so fine-grained observation is free.
|
|
212
|
-
- **Requires iOS 17+ minimum deployment target.** `ObservableObject` is not formally deprecated (no compiler warning) -- it is superseded. Do not rewrite working `ObservableObject` code if the project targets iOS 16 or earlier.
|
|
183
|
+
1. Replace the `ObservableObject` conformance with the `@Observable` macro.
|
|
184
|
+
2. Delete every `@Published`; tracking is automatic.
|
|
185
|
+
3. Change owned instances from `@StateObject` to `@State`. The body stays as it was:
|
|
186
|
+
`$settings.fontSize` and the other bindings still work.
|
|
213
187
|
|
|
214
|
-
|
|
188
|
+
Notes:
|
|
215
189
|
|
|
216
|
-
|
|
190
|
+
- Computed properties built on stored ones are tracked without extra work.
|
|
191
|
+
- A view re-evaluates only when a property it actually read changes, at no
|
|
192
|
+
extra cost.
|
|
193
|
+
- Needs an iOS 17 deployment target.
|
|
194
|
+
- `ObservableObject` is superseded, not formally deprecated, and the compiler
|
|
195
|
+
does not warn about it. On projects that target iOS 16 or earlier, leave
|
|
196
|
+
working `ObservableObject` code alone.
|
|
217
197
|
|
|
218
|
-
|
|
198
|
+
## 4. @ObservedObject to let or @Bindable
|
|
199
|
+
|
|
200
|
+
Before:
|
|
219
201
|
|
|
220
202
|
```swift
|
|
221
|
-
struct
|
|
222
|
-
@ObservedObject var
|
|
203
|
+
struct MemberCard: View {
|
|
204
|
+
@ObservedObject var member: Member
|
|
223
205
|
|
|
224
206
|
var body: some View {
|
|
225
|
-
TextField("
|
|
226
|
-
Toggle("
|
|
207
|
+
TextField("Nickname", text: $member.nickname)
|
|
208
|
+
Toggle("Visible", isOn: $member.isVisible)
|
|
227
209
|
}
|
|
228
210
|
}
|
|
229
211
|
```
|
|
230
212
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
When you only need to read properties, use a plain `let`:
|
|
213
|
+
After, read only:
|
|
234
214
|
|
|
235
215
|
```swift
|
|
236
|
-
struct
|
|
237
|
-
let
|
|
238
|
-
|
|
239
|
-
var body: some View {
|
|
240
|
-
Text(profile.name)
|
|
241
|
-
Text(profile.isPublic ? "Public" : "Private")
|
|
242
|
-
}
|
|
216
|
+
struct MemberBadge: View {
|
|
217
|
+
let member: Member
|
|
218
|
+
var body: some View { Text(member.nickname) }
|
|
243
219
|
}
|
|
244
220
|
```
|
|
245
221
|
|
|
246
|
-
|
|
222
|
+
After, with bindings:
|
|
247
223
|
|
|
248
224
|
```swift
|
|
249
|
-
struct
|
|
250
|
-
@Bindable var
|
|
225
|
+
struct MemberCard: View {
|
|
226
|
+
@Bindable var member: Member
|
|
251
227
|
|
|
252
228
|
var body: some View {
|
|
253
|
-
TextField("
|
|
254
|
-
Toggle("
|
|
229
|
+
TextField("Nickname", text: $member.nickname)
|
|
230
|
+
Toggle("Visible", isOn: $member.isVisible)
|
|
255
231
|
}
|
|
256
232
|
}
|
|
257
233
|
```
|
|
258
234
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
235
|
+
With `@Observable` types, `@ObservedObject` has no role. A plain `let` already
|
|
236
|
+
updates the view for every property it reads; `@Bindable` is only for `$`
|
|
237
|
+
bindings.
|
|
262
238
|
|
|
263
|
-
|
|
239
|
+
## 5. @EnvironmentObject to @Environment
|
|
264
240
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
### Before (Superseded)
|
|
241
|
+
Before:
|
|
268
242
|
|
|
269
243
|
```swift
|
|
270
|
-
|
|
271
|
-
ContentView()
|
|
272
|
-
.environmentObject(authManager)
|
|
273
|
-
|
|
274
|
-
// Usage
|
|
275
|
-
struct ContentView: View {
|
|
276
|
-
@EnvironmentObject var auth: AuthManager
|
|
244
|
+
RootView().environmentObject(session)
|
|
277
245
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
} else {
|
|
282
|
-
LoginView()
|
|
283
|
-
}
|
|
284
|
-
}
|
|
246
|
+
struct GateView: View {
|
|
247
|
+
@EnvironmentObject var session: Session
|
|
248
|
+
var body: some View { session.isSignedIn ? AnyView(HomeView()) : AnyView(SignInView()) }
|
|
285
249
|
}
|
|
286
250
|
```
|
|
287
251
|
|
|
288
|
-
|
|
252
|
+
After (iOS 17+):
|
|
289
253
|
|
|
290
254
|
```swift
|
|
291
|
-
|
|
292
|
-
ContentView()
|
|
293
|
-
.environment(authManager)
|
|
255
|
+
RootView().environment(session)
|
|
294
256
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
@Environment(AuthManager.self) private var auth
|
|
257
|
+
struct GateView: View {
|
|
258
|
+
@Environment(Session.self) private var session
|
|
298
259
|
|
|
299
260
|
var body: some View {
|
|
300
|
-
if
|
|
301
|
-
HomeView()
|
|
302
|
-
} else {
|
|
303
|
-
LoginView()
|
|
304
|
-
}
|
|
261
|
+
if session.isSignedIn { HomeView() } else { SignInView() }
|
|
305
262
|
}
|
|
306
263
|
}
|
|
307
264
|
```
|
|
308
265
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
With `@Observable`, use `.environment(_:)` (the type-keyed overload) instead of `.environmentObject(_:)`. Read with `@Environment(Type.self)`. If you need bindings from an environment-injected object, pull it into a local `@Bindable`:
|
|
266
|
+
Bindings from an environment object come from a local `@Bindable` copy:
|
|
312
267
|
|
|
313
268
|
```swift
|
|
314
|
-
|
|
315
|
-
@
|
|
316
|
-
|
|
317
|
-
var body: some View {
|
|
318
|
-
@Bindable var auth = auth
|
|
319
|
-
Toggle("Remember Me", isOn: $auth.rememberMe)
|
|
320
|
-
}
|
|
269
|
+
var body: some View {
|
|
270
|
+
@Bindable var session = session
|
|
271
|
+
Toggle("Stay signed in", isOn: $session.staySignedIn)
|
|
321
272
|
}
|
|
322
273
|
```
|
|
323
274
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
## foregroundColor(_:) to foregroundStyle(_:)
|
|
327
|
-
|
|
328
|
-
`foregroundColor(_:)` was deprecated in iOS 17. Its replacement, `foregroundStyle(_:)`, accepts any `ShapeStyle` -- not just `Color` -- enabling gradients, hierarchical styles, and materials directly.
|
|
329
|
-
|
|
330
|
-
### Before (Deprecated)
|
|
331
|
-
|
|
332
|
-
```swift
|
|
333
|
-
Text("Hello")
|
|
334
|
-
.foregroundColor(.red)
|
|
335
|
-
|
|
336
|
-
Text("Secondary")
|
|
337
|
-
.foregroundColor(.secondary)
|
|
338
|
-
```
|
|
275
|
+
## 6. foregroundColor to foregroundStyle
|
|
339
276
|
|
|
340
|
-
|
|
277
|
+
`foregroundColor(_:)` is marked for deprecation and renamed to
|
|
278
|
+
`foregroundStyle(_:)`; the compiler does not warn yet. `foregroundStyle(_:)`
|
|
279
|
+
(iOS 15+) takes any `ShapeStyle`, so gradients, hierarchical styles and
|
|
280
|
+
materials work directly.
|
|
341
281
|
|
|
342
282
|
```swift
|
|
343
|
-
Text("
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Gradient -- not possible with foregroundColor
|
|
350
|
-
Text("Gradient")
|
|
351
|
-
.foregroundStyle(
|
|
352
|
-
.linearGradient(colors: [.blue, .purple],
|
|
353
|
-
startPoint: .leading, endPoint: .trailing)
|
|
354
|
-
)
|
|
283
|
+
Text("Overdue").foregroundStyle(.red)
|
|
284
|
+
Text("Updated 2 min ago").foregroundStyle(.secondary)
|
|
285
|
+
Text("Premium")
|
|
286
|
+
.foregroundStyle(.linearGradient(colors: [.orange, .pink],
|
|
287
|
+
startPoint: .leading, endPoint: .trailing))
|
|
355
288
|
```
|
|
356
289
|
|
|
357
|
-
|
|
290
|
+
- Passing a `Color` is a drop-in replacement.
|
|
291
|
+
- Also accepts `.tint`, `.selection` and the hierarchical `.primary`,
|
|
292
|
+
`.secondary`, `.tertiary`, `.quaternary`. A gradient was never possible with
|
|
293
|
+
`foregroundColor`.
|
|
294
|
+
- `foregroundStyle(_:_:)` and `foregroundStyle(_:_:_:)` set two or three
|
|
295
|
+
hierarchy levels for child content in one call.
|
|
296
|
+
- `NSAttributedString.Key.foregroundColor` is unrelated: it is the Foundation
|
|
297
|
+
and UIKit attribute key used with `NSAttributedString`, Core Text and PDF
|
|
298
|
+
rendering. It is not deprecated and has no SwiftUI counterpart.
|
|
358
299
|
|
|
359
|
-
|
|
300
|
+
## 7. Single-Value onChange to Two-Value onChange
|
|
360
301
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
## .onChange(of:perform:) to Two-Value onChange
|
|
366
|
-
|
|
367
|
-
The single-value `onChange` closure was deprecated in iOS 17. The new signature provides both the old and new values.
|
|
368
|
-
|
|
369
|
-
### Before (Deprecated)
|
|
302
|
+
The `onChange(of:perform:)` form whose closure takes one value is deprecated in
|
|
303
|
+
iOS 17.
|
|
370
304
|
|
|
371
305
|
```swift
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### After (Modern)
|
|
306
|
+
// Before
|
|
307
|
+
.onChange(of: volume) { newVolume in player.setVolume(newVolume) }
|
|
378
308
|
|
|
379
|
-
|
|
380
|
-
.onChange(of:
|
|
381
|
-
|
|
309
|
+
// After (iOS 17+)
|
|
310
|
+
.onChange(of: volume) { oldVolume, newVolume in
|
|
311
|
+
if newVolume > oldVolume { showBoostHint = true }
|
|
312
|
+
player.setVolume(newVolume)
|
|
382
313
|
}
|
|
314
|
+
.onChange(of: volume) { _, newVolume in player.setVolume(newVolume) }
|
|
315
|
+
.onChange(of: volume, initial: true) { _, newVolume in player.setVolume(newVolume) }
|
|
383
316
|
```
|
|
384
317
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
performSearch(newValue)
|
|
390
|
-
}
|
|
391
|
-
```
|
|
318
|
+
- The closure receives the old and new values; write `_` for an old value you
|
|
319
|
+
do not use.
|
|
320
|
+
- Comparing old and new inline removes the need for a helper state property.
|
|
321
|
+
- `initial: true` also runs the closure when the view first appears.
|
|
392
322
|
|
|
393
|
-
|
|
323
|
+
## 8. ActionSheet to confirmationDialog
|
|
394
324
|
|
|
395
|
-
|
|
325
|
+
Before:
|
|
396
326
|
|
|
397
327
|
```swift
|
|
398
|
-
.
|
|
399
|
-
|
|
328
|
+
.actionSheet(isPresented: $isAsking) {
|
|
329
|
+
ActionSheet(title: Text("Photo"), message: Text("Choose an action"), buttons: [
|
|
330
|
+
.default(Text("Save")) { save() },
|
|
331
|
+
.destructive(Text("Delete")) { delete() },
|
|
332
|
+
.cancel()
|
|
333
|
+
])
|
|
400
334
|
}
|
|
401
335
|
```
|
|
402
336
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
## ActionSheet to confirmationDialog
|
|
406
|
-
|
|
407
|
-
### Before (Deprecated)
|
|
337
|
+
After (iOS 15+):
|
|
408
338
|
|
|
409
339
|
```swift
|
|
410
|
-
.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
buttons: [
|
|
415
|
-
.default(Text("Share")) { shareItem() },
|
|
416
|
-
.destructive(Text("Delete")) { deleteItem() },
|
|
417
|
-
.cancel()
|
|
418
|
-
]
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### After (Modern)
|
|
424
|
-
|
|
425
|
-
```swift
|
|
426
|
-
.confirmationDialog("Choose an action",
|
|
427
|
-
isPresented: $showingOptions,
|
|
428
|
-
titleVisibility: .visible) {
|
|
429
|
-
Button("Share") { shareItem() }
|
|
430
|
-
Button("Delete", role: .destructive) { deleteItem() }
|
|
431
|
-
Button("Cancel", role: .cancel) {}
|
|
340
|
+
.confirmationDialog("Photo", isPresented: $isAsking, titleVisibility: .visible) {
|
|
341
|
+
Button("Save", action: save)
|
|
342
|
+
Button("Delete", role: .destructive, action: delete)
|
|
343
|
+
Button("Cancel", role: .cancel) { }
|
|
432
344
|
} message: {
|
|
433
|
-
Text("
|
|
345
|
+
Text("Choose an action")
|
|
434
346
|
}
|
|
435
347
|
```
|
|
436
348
|
|
|
437
|
-
|
|
349
|
+
- Regular `Button` views with roles replace the `ActionSheet.Button` array.
|
|
350
|
+
- `titleVisibility` controls the title; on iOS it is hidden by default.
|
|
351
|
+
- A cancel button is added for you when you leave it out.
|
|
438
352
|
|
|
439
|
-
|
|
353
|
+
## 9. Legacy Alert to alert with Actions
|
|
440
354
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
## Alert (Legacy) to Modern .alert with Actions
|
|
444
|
-
|
|
445
|
-
### Before (Deprecated)
|
|
355
|
+
Before:
|
|
446
356
|
|
|
447
357
|
```swift
|
|
448
|
-
.alert(isPresented: $
|
|
449
|
-
Alert(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
secondaryButton: .cancel()
|
|
454
|
-
)
|
|
358
|
+
.alert(isPresented: $confirmLeave) {
|
|
359
|
+
Alert(title: Text("Leave group?"),
|
|
360
|
+
message: Text("You can rejoin later."),
|
|
361
|
+
primaryButton: .destructive(Text("Leave")) { leaveGroup() },
|
|
362
|
+
secondaryButton: .cancel())
|
|
455
363
|
}
|
|
456
364
|
```
|
|
457
365
|
|
|
458
|
-
|
|
366
|
+
After (iOS 15+):
|
|
459
367
|
|
|
460
368
|
```swift
|
|
461
|
-
.alert("
|
|
462
|
-
Button("
|
|
463
|
-
Button("Cancel", role: .cancel) {}
|
|
369
|
+
.alert("Leave group?", isPresented: $confirmLeave) {
|
|
370
|
+
Button("Leave", role: .destructive, action: leaveGroup)
|
|
371
|
+
Button("Cancel", role: .cancel) { }
|
|
464
372
|
} message: {
|
|
465
|
-
Text("
|
|
373
|
+
Text("You can rejoin later.")
|
|
466
374
|
}
|
|
467
375
|
```
|
|
468
376
|
|
|
469
|
-
|
|
377
|
+
Passing the data being confirmed:
|
|
470
378
|
|
|
471
379
|
```swift
|
|
472
|
-
.alert("
|
|
473
|
-
Button("
|
|
474
|
-
} message: {
|
|
475
|
-
Text("
|
|
380
|
+
.alert("Remove device?", isPresented: $confirmRemoval, presenting: pendingDevice) { device in
|
|
381
|
+
Button("Remove \(device.name)", role: .destructive) { remove(device) }
|
|
382
|
+
} message: { device in
|
|
383
|
+
Text("\(device.name) will be signed out.")
|
|
476
384
|
}
|
|
477
385
|
```
|
|
478
386
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
The modern alert API accepts a `presenting` parameter to pass data directly into the alert closures, eliminating the need for separate optional state tracking.
|
|
387
|
+
With `presenting:`, both closures receive the item, so no separate optional
|
|
388
|
+
state has to be tracked.
|
|
482
389
|
|
|
483
|
-
|
|
390
|
+
## 10. AnyView to @ViewBuilder and Concrete Types
|
|
484
391
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
### Before (Deprecated Pattern)
|
|
392
|
+
Before:
|
|
488
393
|
|
|
489
394
|
```swift
|
|
490
|
-
func
|
|
491
|
-
switch
|
|
492
|
-
case .
|
|
493
|
-
case .
|
|
494
|
-
case .
|
|
395
|
+
func screen(for step: CheckoutStep) -> AnyView {
|
|
396
|
+
switch step {
|
|
397
|
+
case .cart: AnyView(CartView())
|
|
398
|
+
case .address: AnyView(AddressView())
|
|
399
|
+
case .payment: AnyView(PaymentView())
|
|
495
400
|
}
|
|
496
401
|
}
|
|
497
402
|
```
|
|
498
403
|
|
|
499
|
-
|
|
404
|
+
After:
|
|
500
405
|
|
|
501
406
|
```swift
|
|
502
407
|
@ViewBuilder
|
|
503
|
-
func
|
|
504
|
-
switch
|
|
505
|
-
case .
|
|
506
|
-
case .
|
|
507
|
-
case .
|
|
408
|
+
func screen(for step: CheckoutStep) -> some View {
|
|
409
|
+
switch step {
|
|
410
|
+
case .cart: CartView()
|
|
411
|
+
case .address: AddressView()
|
|
412
|
+
case .payment: PaymentView()
|
|
508
413
|
}
|
|
509
414
|
}
|
|
510
415
|
```
|
|
511
416
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
## .onAppear + Manual Task to .task
|
|
417
|
+
- `AnyView` hides the concrete type, which blocks efficient diffing and
|
|
418
|
+
transitions. `@ViewBuilder` keeps the types, so SwiftUI can preserve identity
|
|
419
|
+
and animate between branches.
|
|
420
|
+
- Keep `AnyView` for APIs that really must store views of different types in
|
|
421
|
+
one collection.
|
|
422
|
+
- On iOS 26 `AnyView` still works and is still a performance anti-pattern.
|
|
519
423
|
|
|
520
|
-
|
|
424
|
+
## 11. onAppear with Task to .task
|
|
521
425
|
|
|
522
426
|
```swift
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
var body: some View {
|
|
527
|
-
List(posts) { post in
|
|
528
|
-
PostRow(post: post)
|
|
529
|
-
}
|
|
530
|
-
.onAppear {
|
|
531
|
-
Task {
|
|
532
|
-
posts = try await fetchPosts()
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
427
|
+
// Before
|
|
428
|
+
.onAppear {
|
|
429
|
+
Task { receipts = try await ledger.receipts() }
|
|
536
430
|
}
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
### After (Modern)
|
|
540
431
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
var body: some View {
|
|
546
|
-
List(posts) { post in
|
|
547
|
-
PostRow(post: post)
|
|
548
|
-
}
|
|
549
|
-
.task {
|
|
550
|
-
do {
|
|
551
|
-
posts = try await fetchPosts()
|
|
552
|
-
} catch {
|
|
553
|
-
// handle error
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
432
|
+
// After
|
|
433
|
+
.task {
|
|
434
|
+
do { receipts = try await ledger.receipts() }
|
|
435
|
+
catch { loadError = error }
|
|
557
436
|
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
### Migration Notes
|
|
561
437
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
.task(id: selectedCategory) {
|
|
566
|
-
posts = try? await fetchPosts(for: selectedCategory)
|
|
438
|
+
// Re-run when a dependency changes
|
|
439
|
+
.task(id: selectedMonth) {
|
|
440
|
+
receipts = (try? await ledger.receipts(in: selectedMonth)) ?? []
|
|
567
441
|
}
|
|
568
442
|
```
|
|
569
443
|
|
|
570
|
-
|
|
444
|
+
`.task` cancels its work when the view disappears, which avoids retain cycles
|
|
445
|
+
and updates to views that are gone. `.task(id:)` restarts when the id changes.
|
|
571
446
|
|
|
572
|
-
##
|
|
573
|
-
|
|
574
|
-
### Before (Deprecated)
|
|
447
|
+
## 12. presentationMode to dismiss
|
|
575
448
|
|
|
576
449
|
```swift
|
|
577
|
-
|
|
578
|
-
|
|
450
|
+
// Before
|
|
451
|
+
@Environment(\.presentationMode) var presentationMode
|
|
452
|
+
Button("Close") { presentationMode.wrappedValue.dismiss() }
|
|
579
453
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
454
|
+
// After (iOS 15+)
|
|
455
|
+
@Environment(\.dismiss) private var dismiss
|
|
456
|
+
Button("Close") { dismiss() }
|
|
586
457
|
```
|
|
587
458
|
|
|
588
|
-
|
|
459
|
+
- `dismiss` is a `DismissAction` that you call directly; there is no
|
|
460
|
+
`wrappedValue`.
|
|
461
|
+
- It closes sheets, full-screen covers and pushed navigation destinations.
|
|
589
462
|
|
|
590
|
-
|
|
591
|
-
struct DetailView: View {
|
|
592
|
-
@Environment(\.dismiss) private var dismiss
|
|
463
|
+
## 13. GeometryReader Overuse to containerRelativeFrame and Layout
|
|
593
464
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
### Migration Notes
|
|
603
|
-
|
|
604
|
-
`dismiss` is a callable `DismissAction`. Call it directly -- no `.wrappedValue` needed. Works for sheets, full-screen covers, and navigation push destinations.
|
|
605
|
-
|
|
606
|
-
---
|
|
465
|
+
`GeometryReader` has a performance cost and makes layout harder to compose.
|
|
466
|
+
The `Layout` protocol arrived in iOS 16 and `containerRelativeFrame` in iOS 17
|
|
467
|
+
for sizing relative to a container.
|
|
607
468
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
GeometryReader has performance costs and complicates layout. iOS 16 introduced the Layout protocol, and iOS 17 added `containerRelativeFrame` for proportional sizing.
|
|
611
|
-
|
|
612
|
-
### Before (Deprecated Pattern)
|
|
469
|
+
Before:
|
|
613
470
|
|
|
614
471
|
```swift
|
|
615
472
|
GeometryReader { proxy in
|
|
616
473
|
HStack(spacing: 0) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
MainContent()
|
|
620
|
-
.frame(width: proxy.size.width * 0.7)
|
|
474
|
+
MapPanel().frame(width: proxy.size.width * 0.65)
|
|
475
|
+
DetailsPanel().frame(width: proxy.size.width * 0.35)
|
|
621
476
|
}
|
|
622
477
|
}
|
|
623
478
|
```
|
|
624
479
|
|
|
625
|
-
|
|
480
|
+
After, iOS 17+:
|
|
626
481
|
|
|
627
482
|
```swift
|
|
628
483
|
HStack(spacing: 0) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
length * 0.3
|
|
632
|
-
}
|
|
633
|
-
MainContent()
|
|
634
|
-
.containerRelativeFrame(.horizontal) { length, _ in
|
|
635
|
-
length * 0.7
|
|
636
|
-
}
|
|
484
|
+
MapPanel().containerRelativeFrame(.horizontal) { width, _ in width * 0.65 }
|
|
485
|
+
DetailsPanel().containerRelativeFrame(.horizontal) { width, _ in width * 0.35 }
|
|
637
486
|
}
|
|
638
487
|
```
|
|
639
488
|
|
|
640
|
-
|
|
489
|
+
After, iOS 16+ with a custom layout:
|
|
641
490
|
|
|
642
491
|
```swift
|
|
643
|
-
struct
|
|
644
|
-
var
|
|
492
|
+
struct WeightedRow: Layout {
|
|
493
|
+
var weights: [CGFloat]
|
|
645
494
|
|
|
646
495
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
647
496
|
proposal.replacingUnspecifiedDimensions()
|
|
648
497
|
}
|
|
649
498
|
|
|
650
|
-
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize,
|
|
651
|
-
|
|
499
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize,
|
|
500
|
+
subviews: Subviews, cache: inout ()) {
|
|
501
|
+
guard weights.count == subviews.count else { return }
|
|
502
|
+
let total = weights.reduce(0, +)
|
|
652
503
|
var x = bounds.minX
|
|
653
|
-
for (
|
|
654
|
-
let width = bounds.width *
|
|
504
|
+
for (subview, weight) in zip(subviews, weights) {
|
|
505
|
+
let width = bounds.width * weight / total
|
|
655
506
|
subview.place(at: CGPoint(x: x, y: bounds.minY),
|
|
656
507
|
proposal: ProposedViewSize(width: width, height: bounds.height))
|
|
657
508
|
x += width
|
|
658
509
|
}
|
|
659
510
|
}
|
|
660
511
|
}
|
|
661
|
-
|
|
662
|
-
// Usage
|
|
663
|
-
ProportionalHStack(ratios: [0.3, 0.7]) {
|
|
664
|
-
SidePanel()
|
|
665
|
-
MainContent()
|
|
666
|
-
}
|
|
667
512
|
```
|
|
668
513
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
514
|
+
- `GeometryReader` is still right when you truly must read the proposed size
|
|
515
|
+
and cannot state the layout declaratively.
|
|
516
|
+
- Proportional sizing: `containerRelativeFrame`. Custom arrangements: the
|
|
517
|
+
`Layout` protocol. Both avoid the bottom-up sizing of `GeometryReader` that
|
|
518
|
+
makes composition difficult.
|
|
672
519
|
|
|
673
|
-
|
|
520
|
+
## 14. PreviewProvider to the #Preview Macro
|
|
674
521
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
### Before (Deprecated)
|
|
522
|
+
Before:
|
|
678
523
|
|
|
679
524
|
```swift
|
|
680
|
-
struct
|
|
525
|
+
struct BoardingPassView_Previews: PreviewProvider {
|
|
681
526
|
static var previews: some View {
|
|
682
|
-
|
|
683
|
-
.previewDevice("iPhone
|
|
684
|
-
|
|
685
|
-
ContentView()
|
|
527
|
+
BoardingPassView(pass: .sample)
|
|
528
|
+
.previewDevice("iPhone 16 Pro")
|
|
686
529
|
.preferredColorScheme(.dark)
|
|
687
530
|
}
|
|
688
531
|
}
|
|
689
532
|
```
|
|
690
533
|
|
|
691
|
-
|
|
534
|
+
After:
|
|
692
535
|
|
|
693
536
|
```swift
|
|
694
|
-
#Preview("Light
|
|
695
|
-
|
|
537
|
+
#Preview("Light") {
|
|
538
|
+
BoardingPassView(pass: .sample)
|
|
696
539
|
}
|
|
697
540
|
|
|
698
|
-
#Preview("Dark
|
|
699
|
-
|
|
541
|
+
#Preview("Dark") {
|
|
542
|
+
BoardingPassView(pass: .sample)
|
|
700
543
|
.preferredColorScheme(.dark)
|
|
701
544
|
}
|
|
702
545
|
```
|
|
703
546
|
|
|
704
|
-
|
|
547
|
+
Widgets and UIKit:
|
|
705
548
|
|
|
706
549
|
```swift
|
|
707
|
-
#Preview("
|
|
708
|
-
|
|
550
|
+
#Preview("Small", as: .systemSmall) {
|
|
551
|
+
StepsWidget()
|
|
709
552
|
} timeline: {
|
|
710
|
-
|
|
553
|
+
StepsEntry(date: .now, steps: 4_200)
|
|
711
554
|
}
|
|
712
555
|
|
|
713
|
-
#Preview
|
|
714
|
-
let
|
|
715
|
-
|
|
716
|
-
return
|
|
556
|
+
#Preview {
|
|
557
|
+
let controller = ScannerViewController()
|
|
558
|
+
controller.title = "Scanner"
|
|
559
|
+
return controller
|
|
717
560
|
}
|
|
718
561
|
```
|
|
719
562
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
563
|
+
- `#Preview` needs Xcode 15 and less boilerplate, and names each preview
|
|
564
|
+
directly. SwiftUI view previews work with any deployment target; the UIKit
|
|
565
|
+
and WidgetKit forms need iOS 17.
|
|
566
|
+
- Migration: delete the whole `PreviewProvider` struct and write `#Preview`
|
|
567
|
+
blocks in its place.
|
|
725
568
|
|
|
726
|
-
## XCTest to Swift Testing
|
|
569
|
+
## 15. XCTest to Swift Testing
|
|
727
570
|
|
|
728
|
-
Swift Testing
|
|
571
|
+
Swift Testing needs Xcode 16 and runs alongside XCTest.
|
|
729
572
|
|
|
730
|
-
|
|
573
|
+
Before:
|
|
731
574
|
|
|
732
575
|
```swift
|
|
733
|
-
|
|
734
|
-
|
|
576
|
+
final class FareCalculatorTests: XCTestCase {
|
|
577
|
+
var calculator: FareCalculator!
|
|
735
578
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
override func setUp() {
|
|
740
|
-
cart = Cart()
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
override func tearDown() {
|
|
744
|
-
cart = nil
|
|
745
|
-
}
|
|
579
|
+
override func setUp() { calculator = FareCalculator(currency: "EUR") }
|
|
580
|
+
override func tearDown() { calculator = nil }
|
|
746
581
|
|
|
747
|
-
func
|
|
748
|
-
|
|
749
|
-
XCTAssertEqual(cart.items.count, 1)
|
|
750
|
-
XCTAssertEqual(cart.total, 9.99, accuracy: 0.01)
|
|
582
|
+
func testBaseFare() {
|
|
583
|
+
XCTAssertEqual(calculator.fare(km: 10), 12.5, accuracy: 0.001)
|
|
751
584
|
}
|
|
752
585
|
|
|
753
|
-
func
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
func testDiscountCodes() throws {
|
|
758
|
-
let codes = ["SAVE10", "SAVE20", "SAVE50"]
|
|
759
|
-
for code in codes {
|
|
760
|
-
cart.applyDiscount(code: code)
|
|
761
|
-
XCTAssertTrue(cart.hasDiscount)
|
|
586
|
+
func testFaresArePositive() {
|
|
587
|
+
for km in [1, 5, 50] {
|
|
588
|
+
XCTAssertTrue(calculator.fare(km: km) > 0)
|
|
762
589
|
}
|
|
763
590
|
}
|
|
764
591
|
}
|
|
765
592
|
```
|
|
766
593
|
|
|
767
|
-
|
|
594
|
+
After:
|
|
768
595
|
|
|
769
596
|
```swift
|
|
770
597
|
import Testing
|
|
771
|
-
@testable import MyApp
|
|
772
598
|
|
|
773
|
-
@Suite("
|
|
774
|
-
struct
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
@Test("Adding an item updates count and total")
|
|
778
|
-
func addItem() {
|
|
779
|
-
cart.add(Item(name: "Widget", price: 9.99))
|
|
780
|
-
#expect(cart.items.count == 1)
|
|
781
|
-
#expect(cart.total.isApproximatelyEqual(to: 9.99))
|
|
782
|
-
}
|
|
599
|
+
@Suite("Fare calculator")
|
|
600
|
+
struct FareCalculatorTests {
|
|
601
|
+
let calculator = FareCalculator(currency: "EUR")
|
|
783
602
|
|
|
784
|
-
@Test("
|
|
785
|
-
func
|
|
786
|
-
#expect(
|
|
603
|
+
@Test("Base fare for ten kilometres")
|
|
604
|
+
func baseFare() {
|
|
605
|
+
#expect(abs(calculator.fare(km: 10) - 12.5) < 0.001)
|
|
787
606
|
}
|
|
788
607
|
|
|
789
|
-
@Test("
|
|
790
|
-
func
|
|
791
|
-
|
|
792
|
-
#expect(cart.hasDiscount)
|
|
608
|
+
@Test("Fares are positive", arguments: [1, 5, 50])
|
|
609
|
+
func positive(km: Int) {
|
|
610
|
+
#expect(calculator.fare(km: km) > 0)
|
|
793
611
|
}
|
|
794
612
|
}
|
|
795
613
|
```
|
|
796
614
|
|
|
797
|
-
|
|
615
|
+
Steps:
|
|
798
616
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
617
|
+
1. Replace the `XCTestCase` subclass with a struct marked `@Suite`.
|
|
618
|
+
2. Move `setUp` work into `init` or into stored property defaults. For
|
|
619
|
+
teardown, make the suite a `final class` or an `actor` and use `deinit`; a
|
|
620
|
+
plain struct cannot have one.
|
|
621
|
+
3. Replace `XCTAssert*` calls with `#expect(...)`, and use `#require(...)` where
|
|
622
|
+
the test cannot continue without a value.
|
|
623
|
+
4. Replace loops with `@Test("description", arguments:)`.
|
|
805
624
|
|
|
806
|
-
|
|
625
|
+
Notes:
|
|
807
626
|
|
|
808
|
-
|
|
627
|
+
- XCTest and Swift Testing targets can live in the same project during the
|
|
628
|
+
move.
|
|
629
|
+
- `@Test(.disabled("reason"))` takes the place of skipping with `XCTSkip`;
|
|
630
|
+
`.enabled(if:)` covers conditional skips.
|
|
809
631
|
|
|
810
|
-
|
|
632
|
+
## 16. onDelete and EditButton to swipeActions
|
|
811
633
|
|
|
812
|
-
|
|
813
|
-
struct ItemList: View {
|
|
814
|
-
@State private var items = ["A", "B", "C"]
|
|
634
|
+
Before:
|
|
815
635
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
.onDelete { items.remove(atOffsets: $0) }
|
|
823
|
-
.onMove { items.move(fromOffsets: $0, toOffset: $1) }
|
|
824
|
-
}
|
|
825
|
-
.navigationTitle("Items")
|
|
826
|
-
.toolbar { EditButton() }
|
|
827
|
-
}
|
|
636
|
+
```swift
|
|
637
|
+
NavigationView {
|
|
638
|
+
List {
|
|
639
|
+
ForEach(alarms) { AlarmRow(alarm: $0) }
|
|
640
|
+
.onDelete { alarms.remove(atOffsets: $0) }
|
|
641
|
+
.onMove { alarms.move(fromOffsets: $0, toOffset: $1) }
|
|
828
642
|
}
|
|
643
|
+
.toolbar { EditButton() }
|
|
829
644
|
}
|
|
830
645
|
```
|
|
831
646
|
|
|
832
|
-
|
|
647
|
+
After:
|
|
833
648
|
|
|
834
649
|
```swift
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
Button("Delete", role: .destructive) {
|
|
845
|
-
if let index = items.firstIndex(of: item) {
|
|
846
|
-
items.remove(at: index)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
.swipeActions(edge: .leading) {
|
|
851
|
-
Button("Pin", systemImage: "pin") {
|
|
852
|
-
pinItem(item)
|
|
853
|
-
}
|
|
854
|
-
.tint(.orange)
|
|
855
|
-
}
|
|
650
|
+
NavigationStack {
|
|
651
|
+
List {
|
|
652
|
+
ForEach(alarms) { alarm in
|
|
653
|
+
AlarmRow(alarm: alarm)
|
|
654
|
+
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
|
655
|
+
Button("Delete", role: .destructive) { remove(alarm) }
|
|
656
|
+
}
|
|
657
|
+
.swipeActions(edge: .leading) {
|
|
658
|
+
Button("Snooze") { snooze(alarm) }.tint(.orange)
|
|
856
659
|
}
|
|
857
|
-
.onMove { items.move(fromOffsets: $0, toOffset: $1) }
|
|
858
|
-
}
|
|
859
|
-
.navigationTitle("Items")
|
|
860
|
-
.toolbar { EditButton() }
|
|
861
660
|
}
|
|
661
|
+
.onMove { alarms.move(fromOffsets: $0, toOffset: $1) }
|
|
862
662
|
}
|
|
663
|
+
.toolbar { EditButton() }
|
|
863
664
|
}
|
|
864
665
|
```
|
|
865
666
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
667
|
+
- `.swipeActions` (iOS 15+) supports several actions per row, each with its own
|
|
668
|
+
tint and role.
|
|
669
|
+
- `EditButton` and `.onMove` are still fine for reordering. The main change is
|
|
670
|
+
turning `.onDelete` into a destructive swipe action.
|
|
869
671
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
## UIApplication.shared.open to `@Environment(\.openURL)`
|
|
873
|
-
|
|
874
|
-
### Before (Deprecated Pattern)
|
|
672
|
+
## 17. UIApplication.shared.open to openURL
|
|
875
673
|
|
|
876
674
|
```swift
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
UIApplication.shared.open(url)
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
```
|
|
883
|
-
|
|
884
|
-
### After (Modern)
|
|
675
|
+
// Before
|
|
676
|
+
Button("Terms") { UIApplication.shared.open(termsURL) }
|
|
885
677
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
678
|
+
// After
|
|
679
|
+
@Environment(\.openURL) private var openURL
|
|
680
|
+
Button("Terms") { openURL(termsURL) }
|
|
889
681
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
With a completion handler:
|
|
899
|
-
|
|
900
|
-
```swift
|
|
901
|
-
openURL(url) { accepted in
|
|
902
|
-
if !accepted {
|
|
903
|
-
// handle failure to open URL
|
|
682
|
+
// Handling failure
|
|
683
|
+
Button("Terms") {
|
|
684
|
+
openURL(termsURL) { opened in
|
|
685
|
+
if !opened { showLinkError = true }
|
|
904
686
|
}
|
|
905
687
|
}
|
|
906
688
|
```
|
|
907
689
|
|
|
908
|
-
|
|
690
|
+
- `openURL` works on every Apple platform, and it can be replaced in the
|
|
691
|
+
environment to intercept or test link handling.
|
|
692
|
+
- Keep `UIApplication.shared` out of SwiftUI views.
|
|
909
693
|
|
|
910
|
-
|
|
694
|
+
## 18. @FetchRequest to @Query
|
|
911
695
|
|
|
912
|
-
|
|
696
|
+
Once the models move from Core Data to SwiftData, the `@Query` macro (iOS 17+)
|
|
697
|
+
supersedes `@FetchRequest`.
|
|
913
698
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
Core Data's `@FetchRequest` is superseded by SwiftData's `@Query` macro when you migrate to SwiftData models.
|
|
917
|
-
|
|
918
|
-
### Before (Core Data)
|
|
699
|
+
Before:
|
|
919
700
|
|
|
920
701
|
```swift
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
var body: some View {
|
|
928
|
-
List(items) { item in
|
|
929
|
-
Text(item.title ?? "")
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
702
|
+
@FetchRequest(
|
|
703
|
+
sortDescriptors: [NSSortDescriptor(keyPath: \CDExpense.date, ascending: false)],
|
|
704
|
+
predicate: NSPredicate(format: "amount > %f", 100.0)
|
|
705
|
+
)
|
|
706
|
+
private var expenses: FetchedResults<CDExpense>
|
|
933
707
|
```
|
|
934
708
|
|
|
935
|
-
|
|
709
|
+
After:
|
|
936
710
|
|
|
937
711
|
```swift
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
sort: \.timestamp,
|
|
942
|
-
order: .reverse
|
|
943
|
-
) private var items: [Item]
|
|
944
|
-
|
|
945
|
-
var body: some View {
|
|
946
|
-
List(items) { item in
|
|
947
|
-
Text(item.title)
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
712
|
+
@Query(filter: #Predicate<Expense> { $0.amount > 100 },
|
|
713
|
+
sort: \.date, order: .reverse)
|
|
714
|
+
private var expenses: [Expense]
|
|
951
715
|
```
|
|
952
716
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
717
|
+
- `#Predicate` is checked by the compiler, unlike a format string, and sorting
|
|
718
|
+
takes key paths directly.
|
|
719
|
+
- An ancestor view supplies the container with `.modelContainer(for:)`.
|
|
720
|
+
- SwiftData models are ordinary Swift classes marked `@Model`, not
|
|
721
|
+
`NSManagedObject` subclasses.
|
|
958
722
|
|
|
959
|
-
##
|
|
723
|
+
## 19. Type-Erased Returns to some and any
|
|
960
724
|
|
|
961
|
-
|
|
725
|
+
Before:
|
|
962
726
|
|
|
963
727
|
```swift
|
|
964
|
-
func
|
|
965
|
-
AnyView(Text("Hello"))
|
|
966
|
-
}
|
|
728
|
+
func makeHeader() -> AnyView { AnyView(Text("Inbox")) }
|
|
967
729
|
|
|
968
|
-
protocol
|
|
969
|
-
func
|
|
730
|
+
protocol MessageSource {
|
|
731
|
+
func messages() -> AnyPublisher<[Message], Error>
|
|
970
732
|
}
|
|
971
733
|
```
|
|
972
734
|
|
|
973
|
-
|
|
735
|
+
After:
|
|
974
736
|
|
|
975
737
|
```swift
|
|
976
|
-
func
|
|
977
|
-
Text("Hello")
|
|
978
|
-
}
|
|
738
|
+
func makeHeader() -> some View { Text("Inbox") }
|
|
979
739
|
|
|
980
|
-
protocol
|
|
981
|
-
func
|
|
740
|
+
protocol MessageSource: Sendable {
|
|
741
|
+
func messages() async throws -> [Message]
|
|
982
742
|
}
|
|
983
743
|
|
|
984
|
-
|
|
985
|
-
let source: any DataSource = RemoteDataSource()
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
### Migration Notes
|
|
989
|
-
|
|
990
|
-
Use `some` for opaque return types when the concrete type is fixed. Use `any` for existentials when you need to store heterogeneous conformances. Prefer `async throws` over Combine publishers for new code. Swift 5.7+ allows `some` in parameter position too:
|
|
991
|
-
|
|
992
|
-
```swift
|
|
993
|
-
func display(_ view: some View) { ... }
|
|
744
|
+
let source: any MessageSource = RemoteMessageSource()
|
|
994
745
|
```
|
|
995
746
|
|
|
996
|
-
|
|
747
|
+
- `some` marks an opaque result whose concrete type is fixed. `any` marks an
|
|
748
|
+
existential that can hold different conforming types.
|
|
749
|
+
- In new code, prefer `async throws` to Combine publishers.
|
|
750
|
+
- Since Swift 5.7, `some` works in parameter position:
|
|
751
|
+
`func present(_ content: some View)`.
|
|
997
752
|
|
|
998
|
-
## .sheet(
|
|
753
|
+
## 20. sheet(isPresented:) with an Optional to sheet(item:)
|
|
999
754
|
|
|
1000
|
-
|
|
755
|
+
Before, two pieces of state that can disagree:
|
|
1001
756
|
|
|
1002
757
|
```swift
|
|
1003
|
-
@State private var
|
|
1004
|
-
@State private var
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
showingSheet = true
|
|
1009
|
-
} label: {
|
|
1010
|
-
ItemRow(item: item)
|
|
1011
|
-
}
|
|
1012
|
-
.buttonStyle(.plain)
|
|
1013
|
-
.sheet(isPresented: $showingSheet) {
|
|
1014
|
-
if let item = selectedItem {
|
|
1015
|
-
DetailView(item: item)
|
|
1016
|
-
}
|
|
758
|
+
@State private var editingNote: Note?
|
|
759
|
+
@State private var isEditing = false
|
|
760
|
+
|
|
761
|
+
.sheet(isPresented: $isEditing) {
|
|
762
|
+
if let editingNote { NoteEditor(note: editingNote) }
|
|
1017
763
|
}
|
|
1018
764
|
```
|
|
1019
765
|
|
|
1020
|
-
|
|
766
|
+
After:
|
|
1021
767
|
|
|
1022
768
|
```swift
|
|
1023
|
-
@State private var
|
|
769
|
+
@State private var editingNote: Note?
|
|
1024
770
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
} label: {
|
|
1028
|
-
ItemRow(item: item)
|
|
1029
|
-
}
|
|
1030
|
-
.buttonStyle(.plain)
|
|
1031
|
-
.sheet(item: $selectedItem) { item in
|
|
1032
|
-
DetailView(item: item)
|
|
771
|
+
.sheet(item: $editingNote) { note in
|
|
772
|
+
NoteEditor(note: note)
|
|
1033
773
|
}
|
|
1034
774
|
```
|
|
1035
775
|
|
|
1036
|
-
|
|
776
|
+
- `.sheet(item:)` removes the chance of the flag and the value falling out of
|
|
777
|
+
step.
|
|
778
|
+
- The sheet appears when the binding becomes non-nil and closes when it
|
|
779
|
+
returns to nil; the closure receives the unwrapped value.
|
|
1037
780
|
|
|
1038
|
-
|
|
781
|
+
## 21. UIColor Interop to Asset Colors and Color.resolve
|
|
1039
782
|
|
|
1040
|
-
|
|
783
|
+
Building a `Color` from components, directly or through
|
|
784
|
+
`Color(uiColor: UIColor(red:green:blue:alpha:))`, is still fine. Better
|
|
785
|
+
options exist for the two common needs:
|
|
1041
786
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
787
|
+
- Fixed brand colors: an asset catalog color such as `Color("Ocean")` is always
|
|
788
|
+
the first choice; it carries light, dark and high-contrast variants.
|
|
789
|
+
- Reading actual components at runtime: `Color.resolve(in:)` (iOS 17+) returns
|
|
790
|
+
a `Color.Resolved` with `red`, `green`, `blue` and `opacity` for the current
|
|
791
|
+
environment. That covers most of the reasons code used to drop into
|
|
792
|
+
`UIColor`.
|
|
1045
793
|
|
|
1046
794
|
```swift
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
### After (Modern)
|
|
1052
|
-
|
|
1053
|
-
Color initialization from components is still fine, but for dynamic colors prefer:
|
|
795
|
+
struct ContrastBadge: View {
|
|
796
|
+
@Environment(\.self) private var environment
|
|
797
|
+
let tint: Color
|
|
1054
798
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
let
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
799
|
+
var body: some View {
|
|
800
|
+
let resolved = tint.resolve(in: environment)
|
|
801
|
+
let lightBackground = (resolved.red + resolved.green + resolved.blue) / 3 > 0.6
|
|
802
|
+
Text("New")
|
|
803
|
+
.padding(6)
|
|
804
|
+
.background(tint)
|
|
805
|
+
.foregroundStyle(lightBackground ? .black : .white)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
1064
808
|
```
|
|
1065
809
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
`Color.resolve(in:)` (iOS 17+) gives you concrete RGBA values in the current environment, replacing many UIColor interop needs. For custom runtime color manipulations, use resolved colors. For static brand colors, use asset catalogs.
|
|
810
|
+
Use resolved colors for runtime manipulation and asset catalogs for static
|
|
811
|
+
brand colors.
|
|
1069
812
|
|
|
1070
|
-
|
|
813
|
+
## 22. ForEach over a Range to Identifiable Data
|
|
1071
814
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
### Before (Fragile Pattern)
|
|
815
|
+
Before:
|
|
1075
816
|
|
|
1076
817
|
```swift
|
|
1077
|
-
ForEach(0..<
|
|
1078
|
-
Text(
|
|
818
|
+
ForEach(0..<songs.count) { index in
|
|
819
|
+
Text(songs[index].title)
|
|
1079
820
|
}
|
|
1080
821
|
```
|
|
1081
822
|
|
|
1082
|
-
|
|
823
|
+
After:
|
|
1083
824
|
|
|
1084
825
|
```swift
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
Text(item.name)
|
|
826
|
+
ForEach(songs) { song in
|
|
827
|
+
Text(song.title)
|
|
1088
828
|
}
|
|
1089
829
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
Text("\(index + 1). \(item.name)")
|
|
830
|
+
ForEach(Array(songs.enumerated()), id: \.element.id) { position, song in
|
|
831
|
+
Text("\(position + 1). \(song.title)")
|
|
1093
832
|
}
|
|
1094
833
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
TextField("Name", text: $item.name)
|
|
834
|
+
ForEach($songs) { $song in
|
|
835
|
+
TextField("Title", text: $song.title)
|
|
1098
836
|
}
|
|
1099
837
|
```
|
|
1100
838
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
---
|
|
1106
|
-
|
|
1107
|
-
## .toolbar placement consolidation (iOS 26)
|
|
839
|
+
- `ForEach(0..<n)` is safe only when the range is constant. Data that can
|
|
840
|
+
change needs an identifiable collection. The Before code draws a compiler
|
|
841
|
+
warning: "non-constant range: argument must be an integer literal".
|
|
842
|
+
- `ForEach($items)` hands out a binding per element without index arithmetic.
|
|
1108
843
|
|
|
1109
|
-
|
|
844
|
+
## 23. Toolbar Placements navigationBar to topBar
|
|
1110
845
|
|
|
1111
846
|
```swift
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
ToolbarItem(placement: .navigationBarTrailing) {
|
|
1117
|
-
Button("Edit") { isEditing.toggle() }
|
|
1118
|
-
}
|
|
1119
|
-
ToolbarItem(placement: .bottomBar) {
|
|
1120
|
-
Button("Add") { addItem() }
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
```
|
|
1124
|
-
|
|
1125
|
-
### After (Modern)
|
|
847
|
+
// Before
|
|
848
|
+
ToolbarItem(placement: .navigationBarLeading) { closeButton }
|
|
849
|
+
ToolbarItem(placement: .navigationBarTrailing) { saveButton }
|
|
1126
850
|
|
|
1127
|
-
|
|
1128
|
-
.
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
ToolbarItem(placement: .topBarTrailing) {
|
|
1133
|
-
Button("Edit") { isEditing.toggle() }
|
|
1134
|
-
}
|
|
1135
|
-
ToolbarItem(placement: .bottomBar) {
|
|
1136
|
-
Button("Add") { addItem() }
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
851
|
+
// After
|
|
852
|
+
ToolbarItem(placement: .topBarLeading) { closeButton }
|
|
853
|
+
ToolbarItem(placement: .topBarTrailing) { saveButton }
|
|
854
|
+
ToolbarItem(placement: .bottomBar) { statusText }
|
|
1139
855
|
```
|
|
1140
856
|
|
|
1141
|
-
|
|
857
|
+
- `.topBarLeading` and `.topBarTrailing` arrived with the iOS 17 SDK and are
|
|
858
|
+
back-deployed to iOS 14, so they work wherever the old names did. The
|
|
859
|
+
`.navigationBar` names are marked for deprecation in their favour. `.bottomBar` is
|
|
860
|
+
unchanged.
|
|
861
|
+
- The new names behave the same in `NavigationStack` and `NavigationSplitView`.
|
|
862
|
+
Prefer them for consistency across platforms.
|
|
1142
863
|
|
|
1143
|
-
|
|
864
|
+
## 24. cornerRadius to clipShape
|
|
1144
865
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
## cornerRadius to clipShape(.rect(cornerRadius:))
|
|
1148
|
-
|
|
1149
|
-
`.cornerRadius(_:)` was deprecated in iOS 17.
|
|
1150
|
-
|
|
1151
|
-
### Before (Deprecated)
|
|
1152
|
-
|
|
1153
|
-
```swift
|
|
1154
|
-
RoundedRectangle(cornerRadius: 12)
|
|
1155
|
-
.cornerRadius(12)
|
|
1156
|
-
|
|
1157
|
-
Image("photo")
|
|
1158
|
-
.cornerRadius(8)
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
### After (Modern)
|
|
866
|
+
`.cornerRadius(_:)` is marked for deprecation (no compiler warning yet); Apple
|
|
867
|
+
points to `clipShape` instead.
|
|
1162
868
|
|
|
1163
869
|
```swift
|
|
1164
|
-
|
|
870
|
+
Rectangle()
|
|
871
|
+
.fill(.blue)
|
|
872
|
+
.frame(width: 80, height: 80)
|
|
1165
873
|
.clipShape(.rect(cornerRadius: 12))
|
|
1166
874
|
|
|
1167
|
-
Image("
|
|
1168
|
-
.
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
### Migration Notes
|
|
875
|
+
Image("cover")
|
|
876
|
+
.resizable()
|
|
877
|
+
.scaledToFill()
|
|
878
|
+
.clipShape(.rect(cornerRadius: 20))
|
|
1172
879
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
```swift
|
|
1176
|
-
.clipShape(.rect(cornerRadii: .init(topLeading: 12, bottomTrailing: 12)))
|
|
880
|
+
Color.orange
|
|
881
|
+
.clipShape(.rect(cornerRadii: .init(topLeading: 16, bottomTrailing: 16)))
|
|
1177
882
|
```
|
|
1178
883
|
|
|
1179
|
-
|
|
884
|
+
- `.rect(cornerRadius:)` is a `RoundedRectangle`.
|
|
885
|
+
- `.rect(cornerRadii:)` is an `UnevenRoundedRectangle` with a radius per corner.
|
|
886
|
+
- Both shape shorthands need iOS 17. On iOS 16, write
|
|
887
|
+
`.clipShape(RoundedRectangle(cornerRadius: 12))`.
|
|
1180
888
|
|
|
1181
|
-
## tabItem to Tab (iOS 18
|
|
889
|
+
## 25. tabItem to Tab (iOS 18)
|
|
1182
890
|
|
|
1183
|
-
The `
|
|
1184
|
-
|
|
1185
|
-
### Before (Legacy)
|
|
891
|
+
The `Tab` type inside `TabView` (iOS 18+) supersedes the `.tabItem` modifier.
|
|
1186
892
|
|
|
1187
893
|
```swift
|
|
894
|
+
// Before
|
|
1188
895
|
TabView {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
Label("Home", systemImage: "house")
|
|
1192
|
-
}
|
|
1193
|
-
SettingsView()
|
|
1194
|
-
.tabItem {
|
|
1195
|
-
Label("Settings", systemImage: "gear")
|
|
1196
|
-
}
|
|
896
|
+
TodayView().tabItem { Label("Today", systemImage: "sun.max") }
|
|
897
|
+
HistoryView().tabItem { Label("History", systemImage: "clock") }
|
|
1197
898
|
}
|
|
1198
|
-
```
|
|
1199
|
-
|
|
1200
|
-
### After (Modern - iOS 18+)
|
|
1201
899
|
|
|
1202
|
-
|
|
1203
|
-
TabView {
|
|
1204
|
-
Tab("
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
SettingsView()
|
|
900
|
+
// After
|
|
901
|
+
TabView(selection: $selectedTab) {
|
|
902
|
+
Tab("Today", systemImage: "sun.max", value: .today) { TodayView() }
|
|
903
|
+
Tab("History", systemImage: "clock", value: .history) { HistoryView() }
|
|
904
|
+
TabSection("Collections") {
|
|
905
|
+
Tab("Favorites", systemImage: "star", value: AppTab.favorites) { FavoritesView() }
|
|
1209
906
|
}
|
|
1210
907
|
}
|
|
1211
908
|
```
|
|
1212
909
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
910
|
+
- The iPadOS 18 tab sidebar needs `Tab`. `.tabItem` still compiles but its
|
|
911
|
+
tabs cannot appear in the sidebar.
|
|
912
|
+
- Programmatic switching: give each `Tab` a `value` and bind the selection to
|
|
913
|
+
`@State`.
|
|
914
|
+
- `TabSection` groups tabs in the sidebar. Inside it, name the selection type
|
|
915
|
+
(`AppTab.favorites`, where `selectedTab` is an `AppTab`); a bare `.favorites`
|
|
916
|
+
leaves `TabSection`'s generic parameter uninferred and fails to compile.
|
|
1218
917
|
|
|
1219
|
-
##
|
|
918
|
+
## 26. showsIndicators to scrollIndicators
|
|
1220
919
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
### Before
|
|
920
|
+
`ScrollView(_:showsIndicators:)` still exists, but the `.scrollIndicators`
|
|
921
|
+
modifier (iOS 16+) is the consistent way to control indicators.
|
|
1224
922
|
|
|
1225
923
|
```swift
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1229
|
-
```
|
|
1230
|
-
|
|
1231
|
-
### After (Modern)
|
|
924
|
+
// Before
|
|
925
|
+
ScrollView(.vertical, showsIndicators: false) { timeline }
|
|
1232
926
|
|
|
1233
|
-
|
|
1234
|
-
ScrollView {
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
.scrollIndicators(.hidden)
|
|
927
|
+
// After
|
|
928
|
+
ScrollView { timeline }
|
|
929
|
+
.scrollIndicators(.hidden)
|
|
1238
930
|
```
|
|
1239
931
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
932
|
+
- `.scrollIndicators(_:axes:)` accepts `.automatic`, `.visible`, `.hidden` and
|
|
933
|
+
`.never`.
|
|
934
|
+
- It also affects other scrollable views, such as `List` and `TextEditor`.
|
|
935
|
+
- The `axes` parameter sets horizontal and vertical indicators separately.
|