@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +285 -285
- package/package.json +3 -3
- package/pipeline/lib/redact.mjs +3 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/pre-commit-check.sh +4 -0
- package/pipeline/skills/.skill-manifest.json +69 -69
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,46 +1,43 @@
|
|
|
1
1
|
# Design Polish
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
- [Haptics](#haptics)
|
|
7
|
-
- [Matched Transitions](#matched-transitions)
|
|
8
|
-
- [Loading and Placeholders](#loading-and-placeholders)
|
|
9
|
-
- [Focus Handling](#focus-handling)
|
|
10
|
-
|
|
11
|
-
## HIG Alignment
|
|
12
|
-
|
|
13
|
-
iOS Human Interface Guidelines patterns for layout, typography, color, accessibility, and feedback in SwiftUI.
|
|
14
|
-
|
|
15
|
-
### Contents
|
|
16
|
-
|
|
17
|
-
- [Layout and Spacing](#layout-and-spacing)
|
|
18
|
-
- [Typography](#typography)
|
|
19
|
-
- [Color System](#color-system)
|
|
20
|
-
- [Navigation Patterns](#navigation-patterns)
|
|
21
|
-
- [Feedback](#feedback)
|
|
22
|
-
- [Accessibility](#accessibility)
|
|
23
|
-
- [Error and Empty States](#error-and-empty-states)
|
|
24
|
-
|
|
25
|
-
### Layout and Spacing
|
|
26
|
-
|
|
27
|
-
#### Spacing Grid
|
|
3
|
+
Human Interface Guidelines applied in SwiftUI (layout, type, color, navigation
|
|
4
|
+
chrome, feedback, accessibility, empty states), plus theming, haptics, matched
|
|
5
|
+
transitions, loading placeholders and form focus.
|
|
28
6
|
|
|
29
|
-
|
|
7
|
+
## Contents
|
|
30
8
|
|
|
31
|
-
|
|
9
|
+
1. [HIG: Layout and Spacing](#1-hig-layout-and-spacing)
|
|
10
|
+
2. [HIG: Typography](#2-hig-typography)
|
|
11
|
+
3. [HIG: Color](#3-hig-color)
|
|
12
|
+
4. [HIG: Navigation Chrome](#4-hig-navigation-chrome)
|
|
13
|
+
5. [HIG: Feedback](#5-hig-feedback)
|
|
14
|
+
6. [HIG: Accessibility](#6-hig-accessibility)
|
|
15
|
+
7. [HIG: Error and Empty States](#7-hig-error-and-empty-states)
|
|
16
|
+
8. [Theming and Dynamic Type](#8-theming-and-dynamic-type)
|
|
17
|
+
9. [Haptics](#9-haptics)
|
|
18
|
+
10. [Core Haptics](#10-core-haptics)
|
|
19
|
+
11. [Matched Transitions](#11-matched-transitions)
|
|
20
|
+
12. [Loading and Placeholders](#12-loading-and-placeholders)
|
|
21
|
+
13. [Form Focus](#13-form-focus)
|
|
22
|
+
|
|
23
|
+
## 1. HIG: Layout and Spacing
|
|
24
|
+
|
|
25
|
+
Start by leaving `spacing:` off stacks; the adaptive default is usually right.
|
|
26
|
+
When you do choose a value on purpose, take it from a 4 point grid. That grid is
|
|
27
|
+
a widespread design convention, not something Apple requires, and values
|
|
28
|
+
between its stops should not be invented.
|
|
32
29
|
|
|
33
30
|
| Points | Token | Typical use |
|
|
34
|
-
|
|
35
|
-
| 4 |
|
|
36
|
-
| 8 |
|
|
37
|
-
| 12 |
|
|
38
|
-
| 16 |
|
|
39
|
-
| 20 |
|
|
40
|
-
| 24 |
|
|
41
|
-
| 32 |
|
|
42
|
-
| 40 |
|
|
43
|
-
| 48 |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 4 | `xxSmall` | Icon to label, inline badge offset |
|
|
33
|
+
| 8 | `xSmall` | Related items in one group, tight stack gaps |
|
|
34
|
+
| 12 | `small` | Row inner padding, label to secondary text |
|
|
35
|
+
| 16 | `medium` | Standard margin, default gap between sections |
|
|
36
|
+
| 20 | `mediumLarge` | Room between separate controls |
|
|
37
|
+
| 24 | `large` | Section separators, card inner padding |
|
|
38
|
+
| 32 | `xLarge` | Major groups, header to content |
|
|
39
|
+
| 40 | `xxLarge` | Large section breaks |
|
|
40
|
+
| 48 | `xxxLarge` | Hero, splash and onboarding layouts |
|
|
44
41
|
|
|
45
42
|
```swift
|
|
46
43
|
enum Spacing {
|
|
@@ -56,477 +53,416 @@ enum Spacing {
|
|
|
56
53
|
}
|
|
57
54
|
```
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
Margins shared across screens can be named insets:
|
|
60
57
|
|
|
61
58
|
```swift
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
private enum Margin {
|
|
60
|
+
static let regular: CGFloat = 16
|
|
61
|
+
static let tight: CGFloat = 8
|
|
62
|
+
static let wide: CGFloat = 24
|
|
63
|
+
}
|
|
65
64
|
|
|
66
65
|
extension EdgeInsets {
|
|
67
|
-
static let
|
|
68
|
-
|
|
66
|
+
static let regular = EdgeInsets(top: Margin.regular, leading: Margin.regular,
|
|
67
|
+
bottom: Margin.regular, trailing: Margin.regular)
|
|
68
|
+
static let rowContent = EdgeInsets(top: 12, leading: Margin.regular,
|
|
69
|
+
bottom: 12, trailing: Margin.regular)
|
|
69
70
|
}
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
Respect the safe area and pin persistent actions with `safeAreaInset`:
|
|
73
74
|
|
|
74
75
|
```swift
|
|
75
76
|
ScrollView {
|
|
76
|
-
LazyVStack {
|
|
77
|
-
|
|
78
|
-
ItemRow(item: item)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
.padding(.horizontal)
|
|
77
|
+
LazyVStack { ForEach(lines) { OrderLineRow(line: $0) } }
|
|
78
|
+
.padding(.horizontal)
|
|
82
79
|
}
|
|
83
80
|
.safeAreaInset(edge: .bottom) {
|
|
84
81
|
HStack {
|
|
85
|
-
Button("
|
|
86
|
-
|
|
87
|
-
Spacer()
|
|
88
|
-
Button("Confirm") { }
|
|
89
|
-
.buttonStyle(.borderedProminent)
|
|
82
|
+
Button("Back", action: goBack).buttonStyle(.bordered)
|
|
83
|
+
Button("Place order", action: placeOrder).buttonStyle(.borderedProminent)
|
|
90
84
|
}
|
|
91
85
|
.padding()
|
|
92
86
|
.background(.regularMaterial)
|
|
93
87
|
}
|
|
94
88
|
```
|
|
95
89
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Use `horizontalSizeClass` to adapt between compact and regular widths:
|
|
90
|
+
Adapt compact and regular widths with the size class:
|
|
99
91
|
|
|
100
92
|
```swift
|
|
101
93
|
@Environment(\.horizontalSizeClass) private var sizeClass
|
|
102
94
|
|
|
103
|
-
private var
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
[GridItem(.flexible())]
|
|
107
|
-
case .regular:
|
|
108
|
-
[GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
|
|
109
|
-
default:
|
|
110
|
-
[GridItem(.flexible())]
|
|
111
|
-
}
|
|
95
|
+
private var gridColumns: [GridItem] {
|
|
96
|
+
let count = sizeClass == .regular ? 3 : 1
|
|
97
|
+
return Array(repeating: GridItem(.flexible()), count: count)
|
|
112
98
|
}
|
|
113
99
|
```
|
|
114
100
|
|
|
115
|
-
|
|
101
|
+
## 2. HIG: Typography
|
|
116
102
|
|
|
117
|
-
|
|
103
|
+
Text styles scale with Dynamic Type without extra work. Sizes below are the
|
|
104
|
+
iOS defaults at the Large content size.
|
|
118
105
|
|
|
119
|
-
|
|
106
|
+
| Style | Size and weight | Use |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `.largeTitle` | 34 pt regular | Screen title |
|
|
109
|
+
| `.title` | 28 pt regular | Section header |
|
|
110
|
+
| `.title2` | 22 pt regular | Sub-section header |
|
|
111
|
+
| `.title3` | 20 pt regular | Group header |
|
|
112
|
+
| `.headline` | 17 pt semibold | Row title |
|
|
113
|
+
| `.body` | 17 pt regular | Main content |
|
|
114
|
+
| `.callout` | 16 pt regular | Secondary content |
|
|
115
|
+
| `.subheadline` | 15 pt regular | Supporting text |
|
|
116
|
+
| `.footnote` | 13 pt regular | Tertiary information |
|
|
117
|
+
| `.caption` | 12 pt regular | Labels |
|
|
118
|
+
| `.caption2` | 11 pt regular | Small labels |
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|-------|------|--------|-------|
|
|
123
|
-
| `.largeTitle` | 34pt | Regular | Screen titles |
|
|
124
|
-
| `.title` | 28pt | Regular | Section headers |
|
|
125
|
-
| `.title2` | 22pt | Regular | Sub-section headers |
|
|
126
|
-
| `.title3` | 20pt | Regular | Group headers |
|
|
127
|
-
| `.headline` | 17pt | Semibold | Row titles |
|
|
128
|
-
| `.body` | 17pt | Regular | Primary content |
|
|
129
|
-
| `.callout` | 16pt | Regular | Secondary content |
|
|
130
|
-
| `.subheadline` | 15pt | Regular | Supporting text |
|
|
131
|
-
| `.footnote` | 13pt | Regular | Tertiary info |
|
|
132
|
-
| `.caption` | 12pt | Regular | Labels |
|
|
133
|
-
| `.caption2` | 11pt | Regular | Small labels |
|
|
134
|
-
|
|
135
|
-
#### Custom Font with Dynamic Type
|
|
120
|
+
A custom typeface keeps Dynamic Type when it is tied to a text style:
|
|
136
121
|
|
|
137
122
|
```swift
|
|
138
123
|
extension Font {
|
|
139
|
-
static func
|
|
140
|
-
.custom(
|
|
124
|
+
static func brandBody(_ family: String) -> Font {
|
|
125
|
+
.custom(family, size: 17, relativeTo: .body)
|
|
141
126
|
}
|
|
142
127
|
}
|
|
143
128
|
```
|
|
144
129
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
#### Semantic Colors
|
|
148
|
-
|
|
149
|
-
Use semantic colors for automatic light/dark mode support:
|
|
150
|
-
|
|
151
|
-
```swift
|
|
152
|
-
// Labels
|
|
153
|
-
Color.primary // Primary text
|
|
154
|
-
Color.secondary // Secondary text
|
|
155
|
-
Color(uiColor: .tertiaryLabel)
|
|
156
|
-
|
|
157
|
-
// Backgrounds
|
|
158
|
-
Color(uiColor: .systemBackground)
|
|
159
|
-
Color(uiColor: .secondarySystemBackground)
|
|
160
|
-
Color(uiColor: .systemGroupedBackground)
|
|
161
|
-
|
|
162
|
-
// Fills and Separators
|
|
163
|
-
Color(uiColor: .systemFill)
|
|
164
|
-
Color(uiColor: .separator)
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
#### Tint Colors
|
|
130
|
+
## 3. HIG: Color
|
|
168
131
|
|
|
169
|
-
|
|
170
|
-
// Apply app-wide tint
|
|
171
|
-
ContentView()
|
|
172
|
-
.tint(.blue)
|
|
173
|
-
```
|
|
132
|
+
Semantic colors switch between light and dark appearance on their own.
|
|
174
133
|
|
|
175
|
-
|
|
134
|
+
| Role | Colors |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Labels | `Color.primary`, `Color.secondary`, `Color(uiColor: .tertiaryLabel)` |
|
|
137
|
+
| Backgrounds | `Color(uiColor: .systemBackground)`, `.secondarySystemBackground`, `.systemGroupedBackground` |
|
|
138
|
+
| Fill and divider | `.systemFill` and `.separator`, both wrapped in `Color(uiColor:)` |
|
|
176
139
|
|
|
177
|
-
|
|
140
|
+
- Set the app tint once on the root view, for example `.tint(.indigo)`.
|
|
141
|
+
- Interactive elements use the tint through `.tint(...)` or
|
|
142
|
+
`.foregroundStyle(.tint)`.
|
|
143
|
+
- Destructive actions use red (`Color.red`, or a `.destructive` button role).
|
|
178
144
|
|
|
179
|
-
|
|
145
|
+
## 4. HIG: Navigation Chrome
|
|
180
146
|
|
|
181
|
-
|
|
147
|
+
Multi-column hierarchies on iPad and Mac use `NavigationSplitView`:
|
|
182
148
|
|
|
183
149
|
```swift
|
|
184
150
|
NavigationSplitView {
|
|
185
|
-
List(
|
|
186
|
-
NavigationLink(value:
|
|
151
|
+
List(folders, selection: $selectedFolder) { folder in
|
|
152
|
+
NavigationLink(folder.name, value: folder)
|
|
187
153
|
}
|
|
188
|
-
.navigationTitle("
|
|
154
|
+
.navigationTitle("Folders")
|
|
189
155
|
} detail: {
|
|
190
|
-
if let
|
|
191
|
-
|
|
156
|
+
if let selectedFolder {
|
|
157
|
+
FolderDetail(folder: selectedFolder)
|
|
192
158
|
} else {
|
|
193
|
-
ContentUnavailableView("
|
|
159
|
+
ContentUnavailableView("Pick a folder", systemImage: "sidebar.leading")
|
|
194
160
|
}
|
|
195
161
|
}
|
|
196
162
|
```
|
|
197
163
|
|
|
198
|
-
|
|
164
|
+
Tab-based apps give each tab its own `NavigationStack` inside a `TabView`. The
|
|
165
|
+
full tab patterns are in `swiftui-navigation`.
|
|
199
166
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
#### Toolbar
|
|
167
|
+
Toolbars (`.topBarLeading` and `.topBarTrailing` come with the iOS 17 SDK and
|
|
168
|
+
are back-deployed to iOS 14):
|
|
203
169
|
|
|
204
170
|
```swift
|
|
205
171
|
.toolbar {
|
|
206
|
-
ToolbarItem(placement: .topBarLeading) {
|
|
172
|
+
ToolbarItem(placement: .topBarLeading) {
|
|
173
|
+
EditButton()
|
|
174
|
+
}
|
|
207
175
|
ToolbarItemGroup(placement: .topBarTrailing) {
|
|
208
|
-
Button("
|
|
209
|
-
Button("
|
|
176
|
+
Button("Mark all read", systemImage: "envelope.open", action: markAllRead)
|
|
177
|
+
Button("New", systemImage: "plus", action: createItem)
|
|
210
178
|
}
|
|
211
179
|
ToolbarItemGroup(placement: .bottomBar) {
|
|
212
|
-
Button("Archive", systemImage: "archivebox"
|
|
180
|
+
Button("Archive", systemImage: "archivebox", action: archiveSelection)
|
|
213
181
|
Spacer()
|
|
214
|
-
Text("\(
|
|
182
|
+
Text("\(messages.count) messages")
|
|
183
|
+
.font(.footnote)
|
|
184
|
+
.foregroundStyle(.secondary)
|
|
215
185
|
Spacer()
|
|
216
|
-
Button("
|
|
186
|
+
Button("Compose", systemImage: "square.and.pencil", action: compose)
|
|
217
187
|
}
|
|
218
188
|
}
|
|
219
189
|
```
|
|
220
190
|
|
|
221
|
-
|
|
191
|
+
Search with scopes (`.searchScopes` needs iOS 16.4):
|
|
222
192
|
|
|
223
193
|
```swift
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
194
|
+
enum LibraryScope: String, CaseIterable {
|
|
195
|
+
case titles, authors, tags
|
|
196
|
+
var label: String { rawValue.localizedCapitalized }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
|
|
200
|
+
.searchScopes($scope) {
|
|
201
|
+
ForEach(LibraryScope.allCases, id: \.self) { scope in
|
|
202
|
+
Text(scope.label).tag(scope)
|
|
228
203
|
}
|
|
229
204
|
}
|
|
230
205
|
```
|
|
231
206
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
#### Haptic Feedback
|
|
207
|
+
## 5. HIG: Feedback
|
|
235
208
|
|
|
236
|
-
|
|
209
|
+
For haptics that follow state, use `sensoryFeedback(_:trigger:)` (iOS 17+):
|
|
237
210
|
|
|
238
211
|
```swift
|
|
239
|
-
Button("
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
.sensoryFeedback(.success, trigger: didSave)
|
|
212
|
+
Button("Bookmark") { isBookmarked.toggle() }
|
|
213
|
+
.sensoryFeedback(.success, trigger: isBookmarked)
|
|
243
214
|
|
|
244
|
-
Picker("
|
|
245
|
-
Text(
|
|
246
|
-
Text("Popular").tag(SortOrder.popular)
|
|
215
|
+
Picker("Order", selection: $ordering) {
|
|
216
|
+
ForEach(Ordering.allCases) { Text($0.title).tag($0) }
|
|
247
217
|
}
|
|
248
|
-
.sensoryFeedback(.selection, trigger:
|
|
218
|
+
.sensoryFeedback(.selection, trigger: ordering)
|
|
249
219
|
```
|
|
250
220
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
See the Haptics section below for structured patterns.
|
|
221
|
+
Reach for UIKit feedback generators only when the trigger is imperative and
|
|
222
|
+
comes from UIKit or other code outside SwiftUI views.
|
|
254
223
|
|
|
255
|
-
|
|
224
|
+
## 6. HIG: Accessibility
|
|
256
225
|
|
|
257
|
-
|
|
226
|
+
Combine a row into one VoiceOver element and describe it:
|
|
258
227
|
|
|
259
228
|
```swift
|
|
260
229
|
VStack(alignment: .leading) {
|
|
261
|
-
Text(
|
|
262
|
-
Text(
|
|
263
|
-
HStack {
|
|
264
|
-
Image(systemName: "star.fill")
|
|
265
|
-
Text("\(item.rating, specifier: "%.1f")")
|
|
266
|
-
}
|
|
230
|
+
Text(course.title).font(.headline)
|
|
231
|
+
Text("\(course.score, specifier: "%.1f") out of 5")
|
|
267
232
|
}
|
|
268
233
|
.accessibilityElement(children: .combine)
|
|
269
|
-
.accessibilityLabel(
|
|
270
|
-
.accessibilityValue("
|
|
271
|
-
.accessibilityHint("
|
|
234
|
+
.accessibilityLabel(course.title)
|
|
235
|
+
.accessibilityValue("Rated \(course.score, specifier: "%.1f") out of 5")
|
|
236
|
+
.accessibilityHint("Opens the course overview")
|
|
272
237
|
.accessibilityAddTraits(.isButton)
|
|
273
238
|
```
|
|
274
239
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
Adapt layout for accessibility sizes:
|
|
240
|
+
Switch layout for the accessibility text sizes:
|
|
278
241
|
|
|
279
242
|
```swift
|
|
280
|
-
@Environment(\.dynamicTypeSize) private var
|
|
243
|
+
@Environment(\.dynamicTypeSize) private var typeSize
|
|
281
244
|
|
|
282
245
|
var body: some View {
|
|
283
|
-
if
|
|
284
|
-
VStack(alignment: .leading) {
|
|
285
|
-
leadingContent
|
|
286
|
-
trailingContent
|
|
287
|
-
}
|
|
246
|
+
if typeSize.isAccessibilitySize {
|
|
247
|
+
VStack(alignment: .leading) { avatar; details }
|
|
288
248
|
} else {
|
|
289
|
-
HStack {
|
|
290
|
-
leadingContent
|
|
291
|
-
Spacer()
|
|
292
|
-
trailingContent
|
|
293
|
-
}
|
|
249
|
+
HStack { avatar; details; Spacer() }
|
|
294
250
|
}
|
|
295
251
|
}
|
|
296
252
|
```
|
|
297
253
|
|
|
298
|
-
|
|
254
|
+
## 7. HIG: Error and Empty States
|
|
299
255
|
|
|
300
|
-
|
|
256
|
+
`ContentUnavailableView` covers both. The builder form adds actions:
|
|
301
257
|
|
|
302
258
|
```swift
|
|
303
|
-
// Error state
|
|
304
259
|
ContentUnavailableView {
|
|
305
|
-
Label("
|
|
260
|
+
Label("Sync failed", systemImage: "icloud.slash")
|
|
306
261
|
} description: {
|
|
307
262
|
Text(error.localizedDescription)
|
|
308
263
|
} actions: {
|
|
309
|
-
Button("
|
|
264
|
+
Button("Retry") { Task { await resync() } }
|
|
310
265
|
.buttonStyle(.borderedProminent)
|
|
311
266
|
}
|
|
312
267
|
|
|
313
|
-
// Empty state
|
|
314
268
|
ContentUnavailableView {
|
|
315
|
-
Label("No
|
|
269
|
+
Label("No receipts", systemImage: "doc.text.viewfinder")
|
|
316
270
|
} description: {
|
|
317
|
-
Text("
|
|
271
|
+
Text("Scan a paper receipt to add it here.")
|
|
318
272
|
} actions: {
|
|
319
|
-
Button("
|
|
273
|
+
Button("Scan") { isScannerPresented = true }
|
|
320
274
|
.buttonStyle(.borderedProminent)
|
|
321
275
|
}
|
|
322
276
|
```
|
|
323
277
|
|
|
324
|
-
## Theming and Dynamic Type
|
|
278
|
+
## 8. Theming and Dynamic Type
|
|
325
279
|
|
|
326
|
-
|
|
280
|
+
Goal: theming that grows with the app while view code stays semantic.
|
|
327
281
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
-
|
|
333
|
-
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
- Apply Dynamic Type scaling through text styles, `Font.custom(_:size:relativeTo:)`, or `@ScaledMetric` for numeric layout values.
|
|
337
|
-
|
|
338
|
-
### Example: Theme object
|
|
282
|
+
- One `Theme` object is the only source of colors, fonts and spacing.
|
|
283
|
+
- Install it at the app root; views read it with `@Environment(Theme.self)`.
|
|
284
|
+
- Name values by role (`primaryBackground`, `secondaryBackground`, `label`,
|
|
285
|
+
`tint`) rather than by hue.
|
|
286
|
+
- Put user-facing theme choices on a dedicated settings screen.
|
|
287
|
+
- Scaling with the user's text size comes from three places: built-in text
|
|
288
|
+
styles, a custom font tied to a style via `Font.custom(_:size:relativeTo:)`,
|
|
289
|
+
and `@ScaledMetric` for numeric layout values.
|
|
339
290
|
|
|
340
291
|
```swift
|
|
341
|
-
@MainActor
|
|
342
|
-
@Observable
|
|
292
|
+
@MainActor @Observable
|
|
343
293
|
final class Theme {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
294
|
+
var accent: Color = .teal
|
|
295
|
+
var primaryBackground: Color = .white
|
|
296
|
+
var secondaryBackground: Color = .secondary.opacity(0.12)
|
|
297
|
+
var label: Color = .primary
|
|
298
|
+
var textScale: Double = 1.0
|
|
349
299
|
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Example: inject at app root
|
|
353
300
|
|
|
354
|
-
```swift
|
|
355
301
|
@main
|
|
356
|
-
struct
|
|
357
|
-
|
|
302
|
+
struct JournalApp: App {
|
|
303
|
+
@State private var theme = Theme()
|
|
358
304
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
305
|
+
var body: some Scene {
|
|
306
|
+
WindowGroup {
|
|
307
|
+
EntriesView().environment(theme)
|
|
308
|
+
}
|
|
363
309
|
}
|
|
364
|
-
}
|
|
365
310
|
}
|
|
366
|
-
```
|
|
367
311
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
struct ProfileView: View {
|
|
372
|
-
@Environment(Theme.self) private var theme
|
|
312
|
+
struct EntryCard: View {
|
|
313
|
+
@Environment(Theme.self) private var currentTheme
|
|
314
|
+
let entry: Entry
|
|
373
315
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
316
|
+
var body: some View {
|
|
317
|
+
Text(entry.title)
|
|
318
|
+
.foregroundStyle(currentTheme.label)
|
|
319
|
+
.background(currentTheme.primaryBackground)
|
|
378
320
|
}
|
|
379
|
-
.background(theme.primaryBackground)
|
|
380
|
-
}
|
|
381
321
|
}
|
|
382
322
|
```
|
|
383
323
|
|
|
384
|
-
|
|
324
|
+
Guidelines:
|
|
385
325
|
|
|
386
|
-
- Keep theme values
|
|
387
|
-
|
|
388
|
-
-
|
|
326
|
+
- Keep the set of theme values small and semantic; do not re-create system
|
|
327
|
+
colors.
|
|
328
|
+
- Persist theme choices the user makes, if the app offers them.
|
|
329
|
+
- Check contrast between every text color and its background.
|
|
389
330
|
|
|
390
|
-
|
|
331
|
+
Pitfalls:
|
|
391
332
|
|
|
392
|
-
-
|
|
393
|
-
-
|
|
394
|
-
-
|
|
333
|
+
- Raw `Color` literals spread through views.
|
|
334
|
+
- A theme stored in one view's local state.
|
|
335
|
+
- Treating `@Environment(\.colorScheme)` as the whole theming story. Light or
|
|
336
|
+
dark mode is one input to the theme, not a substitute for it.
|
|
395
337
|
|
|
396
|
-
## Haptics
|
|
338
|
+
## 9. Haptics
|
|
397
339
|
|
|
398
|
-
|
|
340
|
+
Use haptics for moments that matter (tab switch, refresh, success, failure) and
|
|
341
|
+
respect the user's settings.
|
|
399
342
|
|
|
400
|
-
|
|
343
|
+
- In SwiftUI views, prefer `sensoryFeedback(_:trigger:)`.
|
|
344
|
+
- Centralise imperative feedback in a manager only for UIKit interop or code
|
|
345
|
+
that is not a view.
|
|
346
|
+
- Check both the user preference and hardware support.
|
|
347
|
+
- Use different feedback types for different moments: selection, notification,
|
|
348
|
+
refresh.
|
|
349
|
+
- Core Haptics is the step up when a pattern needs shaping that the built-in
|
|
350
|
+
feedback types do not offer.
|
|
401
351
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
- Prefer `sensoryFeedback(_:trigger:)` in SwiftUI views for state-driven feedback.
|
|
405
|
-
- Centralize imperative feedback in a `HapticManager` only when UIKit interop or non-view code requires it.
|
|
406
|
-
- Gate haptics behind user preferences and hardware support.
|
|
407
|
-
- Use distinct types for different UX moments (selection vs. notification vs. refresh).
|
|
408
|
-
- Escalate to Core Haptics only for custom patterns that exceed SwiftUI's built-in feedback types.
|
|
409
|
-
|
|
410
|
-
### SwiftUI-first pattern
|
|
352
|
+
SwiftUI first:
|
|
411
353
|
|
|
412
354
|
```swift
|
|
413
|
-
|
|
414
|
-
@State private var saveToken = 0
|
|
355
|
+
@State private var uploadCount = 0
|
|
415
356
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
saveToken += 1
|
|
420
|
-
}
|
|
421
|
-
.sensoryFeedback(.success, trigger: saveToken)
|
|
422
|
-
}
|
|
357
|
+
Button("Upload") {
|
|
358
|
+
uploadCount += 1
|
|
359
|
+
startUpload()
|
|
423
360
|
}
|
|
361
|
+
.sensoryFeedback(.success, trigger: uploadCount)
|
|
424
362
|
```
|
|
425
363
|
|
|
426
|
-
|
|
364
|
+
UIKit interop:
|
|
427
365
|
|
|
428
366
|
```swift
|
|
367
|
+
import UIKit
|
|
368
|
+
|
|
429
369
|
@MainActor
|
|
430
|
-
final class
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
impactGenerator.impactOccurred(intensity: intensity)
|
|
455
|
-
case let .notification(style):
|
|
456
|
-
notificationGenerator.notificationOccurred(style)
|
|
370
|
+
final class FeedbackPlayer {
|
|
371
|
+
static let shared = FeedbackPlayer()
|
|
372
|
+
|
|
373
|
+
enum Kind {
|
|
374
|
+
case tap
|
|
375
|
+
case tabChange
|
|
376
|
+
case refresh(strength: CGFloat)
|
|
377
|
+
case result(UINotificationFeedbackGenerator.FeedbackType)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private let selection = UISelectionFeedbackGenerator()
|
|
381
|
+
private let impact = UIImpactFeedbackGenerator(style: .heavy)
|
|
382
|
+
private let notification = UINotificationFeedbackGenerator()
|
|
383
|
+
|
|
384
|
+
private init() { selection.prepare() }
|
|
385
|
+
|
|
386
|
+
func play(_ kind: Kind, allowed: Bool) {
|
|
387
|
+
guard allowed else { return }
|
|
388
|
+
switch kind {
|
|
389
|
+
case .tap: impact.impactOccurred()
|
|
390
|
+
case .tabChange: selection.selectionChanged()
|
|
391
|
+
case .refresh(let strength): impact.impactOccurred(intensity: strength)
|
|
392
|
+
case .result(let type): notification.notificationOccurred(type)
|
|
393
|
+
}
|
|
457
394
|
}
|
|
458
|
-
}
|
|
459
395
|
}
|
|
460
396
|
```
|
|
461
397
|
|
|
462
|
-
### Example: usage
|
|
463
|
-
|
|
464
398
|
```swift
|
|
465
|
-
|
|
466
|
-
|
|
399
|
+
@AppStorage("hapticsOn") private var hapticsOn = true
|
|
400
|
+
|
|
401
|
+
Button("Pay") {
|
|
402
|
+
FeedbackPlayer.shared.play(.result(.success), allowed: hapticsOn)
|
|
467
403
|
}
|
|
468
404
|
|
|
469
|
-
TabView(selection: $
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
405
|
+
TabView(selection: $tab) { tabs }
|
|
406
|
+
.onChange(of: tab) {
|
|
407
|
+
FeedbackPlayer.shared.play(.tabChange, allowed: hapticsOn)
|
|
408
|
+
}
|
|
473
409
|
```
|
|
474
410
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
- Haptics should be subtle and not fire on every tiny interaction.
|
|
478
|
-
- Respect user preferences (toggle to disable).
|
|
479
|
-
- Keep haptic triggers close to the user action, not deep in data layers.
|
|
411
|
+
Guidelines:
|
|
480
412
|
|
|
481
|
-
|
|
413
|
+
- Keep feedback subtle and skip it for minor interactions.
|
|
414
|
+
- Offer a setting that turns haptics off.
|
|
415
|
+
- Trigger feedback where the user acts, not from the data layer.
|
|
482
416
|
|
|
483
|
-
|
|
484
|
-
- Do not assume haptics are available; check support.
|
|
417
|
+
Pitfalls:
|
|
485
418
|
|
|
486
|
-
|
|
419
|
+
- Several haptics fired back to back.
|
|
420
|
+
- Assuming the device has a haptic engine.
|
|
487
421
|
|
|
488
|
-
|
|
422
|
+
## 10. Core Haptics
|
|
489
423
|
|
|
490
|
-
|
|
424
|
+
Core Haptics controls intensity, sharpness, timing and audio sync precisely,
|
|
425
|
+
for patterns the simple generators cannot produce. Apple docs:
|
|
426
|
+
[CHHapticEngine](https://developer.apple.com/documentation/corehaptics/chhapticengine),
|
|
427
|
+
and the [Core Haptics](https://developer.apple.com/documentation/corehaptics)
|
|
428
|
+
topic page, whose "Essentials" group holds the article on preparing an app to
|
|
429
|
+
play haptics.
|
|
491
430
|
|
|
492
|
-
|
|
431
|
+
### Engine lifecycle
|
|
493
432
|
|
|
494
|
-
|
|
433
|
+
Check the hardware before creating an engine.
|
|
495
434
|
|
|
496
435
|
```swift
|
|
497
436
|
import CoreHaptics
|
|
437
|
+
import OSLog
|
|
498
438
|
|
|
499
|
-
|
|
500
|
-
let
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
#### Engine setup and lifecycle
|
|
439
|
+
extension Logger {
|
|
440
|
+
static let haptics = Logger(subsystem: "app.haptics", category: "engine")
|
|
441
|
+
}
|
|
504
442
|
|
|
505
|
-
```swift
|
|
506
443
|
@MainActor
|
|
507
|
-
final class
|
|
444
|
+
final class PatternPlayer {
|
|
508
445
|
private var engine: CHHapticEngine?
|
|
509
446
|
|
|
510
|
-
|
|
511
|
-
|
|
447
|
+
var canPlayHaptics: Bool {
|
|
448
|
+
CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
|
449
|
+
}
|
|
512
450
|
|
|
513
|
-
|
|
451
|
+
var canPlayHapticAudio: Bool {
|
|
452
|
+
CHHapticEngine.capabilitiesForHardware().supportsAudio
|
|
453
|
+
}
|
|
514
454
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
455
|
+
func startEngine() throws {
|
|
456
|
+
guard canPlayHaptics else { return }
|
|
457
|
+
let engine = try CHHapticEngine()
|
|
458
|
+
engine.stoppedHandler = { @Sendable reason in
|
|
459
|
+
Logger.haptics.info("Engine stopped, reason \(reason.rawValue)")
|
|
518
460
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
engine?.resetHandler = { [weak self] in
|
|
522
|
-
do {
|
|
523
|
-
try self?.engine?.start()
|
|
524
|
-
} catch {
|
|
525
|
-
print("Failed to restart engine: \(error)")
|
|
526
|
-
}
|
|
461
|
+
engine.resetHandler = { @Sendable [weak self] in
|
|
462
|
+
Task { @MainActor in try? self?.engine?.start() }
|
|
527
463
|
}
|
|
528
|
-
|
|
529
|
-
|
|
464
|
+
try engine.start()
|
|
465
|
+
self.engine = engine
|
|
530
466
|
}
|
|
531
467
|
|
|
532
468
|
func stopEngine() {
|
|
@@ -535,304 +471,316 @@ final class CoreHapticManager {
|
|
|
535
471
|
}
|
|
536
472
|
```
|
|
537
473
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
-
|
|
541
|
-
|
|
542
|
-
-
|
|
543
|
-
|
|
544
|
-
|
|
474
|
+
- `stoppedHandler` runs when something outside the app stops the engine, such
|
|
475
|
+
as an audio session interruption or the app moving to the background.
|
|
476
|
+
- Both handlers are called on a queue owned by the engine, so they are
|
|
477
|
+
`@Sendable`; any state change is scheduled onto the main actor first.
|
|
478
|
+
- `resetHandler` runs after the system resets the engine, for instance once an
|
|
479
|
+
audio interruption is over. Start the engine again there.
|
|
480
|
+
- Call `start()` before playing any pattern.
|
|
481
|
+
- Once the screen no longer plays patterns, `stop()` the engine to save power.
|
|
545
482
|
|
|
546
|
-
|
|
483
|
+
### A single tap
|
|
547
484
|
|
|
548
485
|
```swift
|
|
549
|
-
func
|
|
550
|
-
let
|
|
551
|
-
let
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
relativeTime: 0
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
let pattern = try CHHapticPattern(events: [event], parameters: [])
|
|
561
|
-
let player = try engine?.makePlayer(with: pattern)
|
|
562
|
-
try player?.start(atTime: CHHapticTimeImmediate)
|
|
486
|
+
func playTap() throws {
|
|
487
|
+
let crisp = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.75)
|
|
488
|
+
let strong = CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.95)
|
|
489
|
+
let tap = CHHapticEvent(eventType: .hapticTransient,
|
|
490
|
+
parameters: [crisp, strong],
|
|
491
|
+
relativeTime: 0)
|
|
492
|
+
let pattern = try CHHapticPattern(events: [tap], parameters: [])
|
|
493
|
+
try engine?.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
|
|
563
494
|
}
|
|
564
495
|
```
|
|
565
496
|
|
|
566
|
-
|
|
497
|
+
### Event types and parameters
|
|
567
498
|
|
|
568
|
-
|
|
|
569
|
-
|
|
570
|
-
| `.hapticTransient` |
|
|
571
|
-
| `.hapticContinuous` | Sustained vibration
|
|
499
|
+
| Event type | Effect |
|
|
500
|
+
|---|---|
|
|
501
|
+
| `.hapticTransient` | Short impulse, like a tap |
|
|
502
|
+
| `.hapticContinuous` | Sustained vibration for a `duration` |
|
|
572
503
|
| `.audioContinuous` | Sustained audio tone |
|
|
573
|
-
| `.audioCustom` |
|
|
574
|
-
|
|
575
|
-
**Common parameters:** `.hapticIntensity` (0-1), `.hapticSharpness` (0-1), `.attackTime`, `.decayTime`, `.releaseTime`.
|
|
504
|
+
| `.audioCustom` | Plays an audio resource you register |
|
|
576
505
|
|
|
577
|
-
|
|
506
|
+
Common parameters: `.hapticIntensity` (0 to 1), `.hapticSharpness` (0 to 1),
|
|
507
|
+
`.attackTime`, `.decayTime`, `.releaseTime`.
|
|
578
508
|
|
|
579
509
|
```swift
|
|
580
|
-
func
|
|
581
|
-
let
|
|
582
|
-
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
|
583
|
-
|
|
584
|
-
let event = CHHapticEvent(
|
|
510
|
+
func playHum() throws {
|
|
511
|
+
let hum = CHHapticEvent(
|
|
585
512
|
eventType: .hapticContinuous,
|
|
586
|
-
parameters: [
|
|
513
|
+
parameters: [
|
|
514
|
+
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.55),
|
|
515
|
+
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.35)
|
|
516
|
+
],
|
|
587
517
|
relativeTime: 0,
|
|
588
518
|
duration: 0.5
|
|
589
519
|
)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
let player = try engine?.makePlayer(with: pattern)
|
|
593
|
-
try player?.start(atTime: CHHapticTimeImmediate)
|
|
520
|
+
let humPattern = try CHHapticPattern(events: [hum], parameters: [])
|
|
521
|
+
try engine?.makePlayer(with: humPattern).start(atTime: CHHapticTimeImmediate)
|
|
594
522
|
}
|
|
595
523
|
```
|
|
596
524
|
|
|
597
|
-
|
|
525
|
+
Looping, seeking and pausing need a `CHHapticAdvancedPatternPlayer` from
|
|
526
|
+
`engine.makeAdvancedPlayer(with:)`.
|
|
598
527
|
|
|
599
|
-
|
|
528
|
+
### Parameter curves
|
|
600
529
|
|
|
601
|
-
|
|
530
|
+
A `CHHapticParameterCurve` changes a parameter smoothly while the pattern
|
|
531
|
+
plays:
|
|
602
532
|
|
|
603
533
|
```swift
|
|
604
|
-
func
|
|
605
|
-
let
|
|
534
|
+
func playSwell() throws {
|
|
535
|
+
let base = CHHapticEvent(
|
|
606
536
|
eventType: .hapticContinuous,
|
|
607
537
|
parameters: [
|
|
608
|
-
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.
|
|
609
|
-
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.
|
|
538
|
+
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25),
|
|
539
|
+
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
|
|
610
540
|
],
|
|
611
541
|
relativeTime: 0,
|
|
612
542
|
duration: 1.0
|
|
613
543
|
)
|
|
614
|
-
|
|
615
|
-
// Ramp intensity from 0.2 → 1.0 over 1 second
|
|
616
|
-
let curve = CHHapticParameterCurve(
|
|
544
|
+
let swell = CHHapticParameterCurve(
|
|
617
545
|
parameterID: .hapticIntensityControl,
|
|
618
546
|
controlPoints: [
|
|
619
|
-
.
|
|
620
|
-
.
|
|
621
|
-
.
|
|
547
|
+
CHHapticParameterCurve.ControlPoint(relativeTime: 0, value: 0.25),
|
|
548
|
+
CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.6),
|
|
549
|
+
CHHapticParameterCurve.ControlPoint(relativeTime: 0.9, value: 1)
|
|
622
550
|
],
|
|
623
551
|
relativeTime: 0
|
|
624
552
|
)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
let player = try engine?.makePlayer(with: pattern)
|
|
628
|
-
try player?.start(atTime: CHHapticTimeImmediate)
|
|
553
|
+
let pattern = try CHHapticPattern(events: [base], parameterCurves: [swell])
|
|
554
|
+
try engine?.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
|
|
629
555
|
}
|
|
630
556
|
```
|
|
631
557
|
|
|
632
|
-
|
|
558
|
+
### AHAP files
|
|
633
559
|
|
|
634
|
-
|
|
560
|
+
Patterns can also live outside code, in an AHAP file: a JSON document you
|
|
561
|
+
tune without recompiling.
|
|
635
562
|
|
|
636
563
|
```swift
|
|
637
|
-
func
|
|
638
|
-
guard let
|
|
639
|
-
|
|
564
|
+
func playFromFile() throws {
|
|
565
|
+
guard let patternURL = Bundle.main.url(forResource: "checkout", withExtension: "ahap") else {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
try engine?.playPattern(from: patternURL)
|
|
640
569
|
}
|
|
641
570
|
```
|
|
642
571
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
## Matched Transitions
|
|
572
|
+
Everything the code API expresses (events, parameters, curves) can be written
|
|
573
|
+
in AHAP too, and Xcode can play the file back for you while you iterate. File
|
|
574
|
+
format reference: the AHAP article listed on the
|
|
575
|
+
[Core Haptics](https://developer.apple.com/documentation/corehaptics) topic page.
|
|
648
576
|
|
|
649
|
-
|
|
577
|
+
## 11. Matched Transitions
|
|
650
578
|
|
|
651
|
-
|
|
579
|
+
Goal: carry the eye from a source view (thumbnail, avatar) to its destination
|
|
580
|
+
(sheet, detail, full-screen viewer).
|
|
652
581
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
-
|
|
656
|
-
|
|
657
|
-
-
|
|
658
|
-
-
|
|
659
|
-
|
|
660
|
-
### Example: media preview to full-screen viewer (iOS 26+)
|
|
582
|
+
- Share a `Namespace` between source and destination, and give the source a
|
|
583
|
+
stable id.
|
|
584
|
+
- Across screens, use `matchedTransitionSource(id:in:)` with
|
|
585
|
+
`navigationTransition(.zoom(sourceID:in:))`. Both need iOS 18.
|
|
586
|
+
- Inside one hierarchy, use `matchedGeometryEffect(id:in:)`.
|
|
587
|
+
- Ids must stay the same across updates; never generate random UUIDs for them.
|
|
661
588
|
|
|
662
589
|
```swift
|
|
663
|
-
struct
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
590
|
+
struct GalleryGrid: View {
|
|
591
|
+
@Namespace private var zoom
|
|
592
|
+
@State private var opened: Photo?
|
|
593
|
+
let photos: [Photo]
|
|
594
|
+
|
|
595
|
+
var body: some View {
|
|
596
|
+
LazyVGrid(columns: [GridItem(.adaptive(minimum: 96))]) {
|
|
597
|
+
ForEach(photos) { photo in
|
|
598
|
+
Button { opened = photo } label: {
|
|
599
|
+
PhotoThumbnail(photo: photo)
|
|
600
|
+
}
|
|
601
|
+
.matchedTransitionSource(id: photo.id, in: zoom)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
.sheet(item: $opened) { photo in
|
|
605
|
+
PhotoViewer(photo: photo)
|
|
606
|
+
.navigationTransition(.zoom(sourceID: photo.id, in: zoom))
|
|
607
|
+
}
|
|
608
|
+
}
|
|
675
609
|
}
|
|
676
610
|
```
|
|
677
611
|
|
|
678
|
-
|
|
612
|
+
In-place change within one view:
|
|
679
613
|
|
|
680
614
|
```swift
|
|
681
|
-
struct
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
615
|
+
struct LockToggle: View {
|
|
616
|
+
@Namespace private var space
|
|
617
|
+
@State private var isLocked = false
|
|
618
|
+
|
|
619
|
+
var body: some View {
|
|
620
|
+
Button {
|
|
621
|
+
withAnimation(.spring) { isLocked.toggle() }
|
|
622
|
+
} label: {
|
|
623
|
+
Image(systemName: isLocked ? "lock" : "lock.open")
|
|
624
|
+
.matchedGeometryEffect(id: "lockGlyph", in: space)
|
|
625
|
+
}
|
|
691
626
|
}
|
|
692
|
-
}
|
|
693
627
|
}
|
|
694
628
|
```
|
|
695
629
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
- Prefer `matchedTransitionSource` for cross-screen transitions.
|
|
699
|
-
- Keep source and destination sizes reasonable to avoid jarring scale changes.
|
|
700
|
-
- Use `withAnimation` for state-driven transitions.
|
|
701
|
-
|
|
702
|
-
### Pitfalls
|
|
630
|
+
Guidelines:
|
|
703
631
|
|
|
704
|
-
-
|
|
705
|
-
-
|
|
632
|
+
- Prefer `matchedTransitionSource` for transitions between screens.
|
|
633
|
+
- Keep source and destination sizes close so the scale change is not jarring.
|
|
634
|
+
- Drive state-based transitions with `withAnimation`.
|
|
706
635
|
|
|
707
|
-
|
|
636
|
+
Pitfalls:
|
|
708
637
|
|
|
709
|
-
|
|
638
|
+
- Ids that change between updates break the transition.
|
|
639
|
+
- Shapes that do not match (a square into a circle) look wrong unless the
|
|
640
|
+
design asks for it.
|
|
710
641
|
|
|
711
|
-
|
|
642
|
+
## 12. Loading and Placeholders
|
|
712
643
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
- **ProgressView** only for short, global operations (use sparingly in content-heavy screens).
|
|
644
|
+
Use this for loading states (skeletons, redaction, empty state) that stay
|
|
645
|
+
consistent and never block interaction.
|
|
716
646
|
|
|
717
|
-
|
|
647
|
+
- For lists and detail content, prefer redacted placeholders; the layout stays
|
|
648
|
+
in place while data arrives.
|
|
649
|
+
- Once loading ends with nothing to show, or with an error, show
|
|
650
|
+
`ContentUnavailableView`.
|
|
651
|
+
- Keep `ProgressView` for short, global operations, and use it sparingly on
|
|
652
|
+
content-heavy screens.
|
|
718
653
|
|
|
719
|
-
|
|
720
|
-
2. For lists, show a fixed number of placeholder rows (avoid infinite spinners).
|
|
721
|
-
3. Switch to `ContentUnavailableView` when load finishes but data is empty.
|
|
654
|
+
Steps:
|
|
722
655
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
### Minimal usage
|
|
656
|
+
1. Render the real layout with placeholder data and apply
|
|
657
|
+
`.redacted(reason: .placeholder)`.
|
|
658
|
+
2. A list gets a small, fixed set of skeleton rows, never a spinner that runs
|
|
659
|
+
with no end in sight.
|
|
660
|
+
3. When loading finishes with no data, switch to `ContentUnavailableView`.
|
|
730
661
|
|
|
731
662
|
```swift
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
663
|
+
List {
|
|
664
|
+
if isFetching {
|
|
665
|
+
ForEach(0..<4, id: \.self) { _ in
|
|
666
|
+
InvoiceRow(invoice: .sample)
|
|
667
|
+
.redacted(reason: .placeholder)
|
|
668
|
+
}
|
|
669
|
+
} else if invoices.isEmpty {
|
|
670
|
+
ContentUnavailableView("No invoices", systemImage: "tray")
|
|
671
|
+
} else {
|
|
672
|
+
ForEach(invoices) { InvoiceRow(invoice: $0) }
|
|
736
673
|
}
|
|
737
|
-
.redacted(reason: .placeholder)
|
|
738
|
-
} else if items.isEmpty {
|
|
739
|
-
ContentUnavailableView("No items", systemImage: "tray")
|
|
740
|
-
} else {
|
|
741
|
-
ForEach(items) { item in RowView(model: item) }
|
|
742
|
-
}
|
|
743
674
|
}
|
|
744
675
|
```
|
|
745
676
|
|
|
746
|
-
|
|
677
|
+
Pitfalls and limits:
|
|
747
678
|
|
|
748
|
-
|
|
679
|
+
- Layout that shifts or animates while redacted. Keep frames stable.
|
|
680
|
+
- Spinners nested inside spinners. One indicator per section.
|
|
681
|
+
- More than three to six placeholder rows, which costs frames on older
|
|
682
|
+
devices.
|
|
749
683
|
|
|
750
|
-
|
|
684
|
+
## 13. Form Focus
|
|
751
685
|
|
|
752
|
-
|
|
686
|
+
This section covers keyboard focus in forms. Directional focus, focus sections
|
|
687
|
+
and focus on tvOS, macOS and iPadOS keyboards are covered in `hig-inputs`;
|
|
688
|
+
VoiceOver and other assistive focus is covered in `ios-accessibility`.
|
|
753
689
|
|
|
754
|
-
|
|
690
|
+
`@FocusState` sets keyboard focus, moves it from field to field, and
|
|
691
|
+
coordinates it across a complex form.
|
|
755
692
|
|
|
756
|
-
-
|
|
757
|
-
- Set
|
|
758
|
-
-
|
|
759
|
-
- For
|
|
693
|
+
- Describe the focusable fields with an enum.
|
|
694
|
+
- Set the first focus in `onAppear`.
|
|
695
|
+
- Move to the following field in `.onSubmit`.
|
|
696
|
+
- For a variable number of fields, use an enum case with an associated value,
|
|
697
|
+
such as `.answer(Int)`.
|
|
760
698
|
|
|
761
|
-
|
|
699
|
+
One field:
|
|
762
700
|
|
|
763
701
|
```swift
|
|
764
|
-
struct
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
702
|
+
struct HostEntryForm: View {
|
|
703
|
+
@State private var host = ""
|
|
704
|
+
@FocusState private var hostFocused: Bool
|
|
705
|
+
|
|
706
|
+
var body: some View {
|
|
707
|
+
Form {
|
|
708
|
+
TextField("Host", text: $host)
|
|
709
|
+
.focused($hostFocused)
|
|
710
|
+
}
|
|
711
|
+
.onAppear { hostFocused = true }
|
|
772
712
|
}
|
|
773
|
-
.onAppear { isServerFieldFocused = true }
|
|
774
|
-
}
|
|
775
713
|
}
|
|
776
714
|
```
|
|
777
715
|
|
|
778
|
-
|
|
716
|
+
Chained fields:
|
|
779
717
|
|
|
780
718
|
```swift
|
|
781
|
-
struct
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
719
|
+
struct ContactForm: View {
|
|
720
|
+
enum Field { case name, phone, note }
|
|
721
|
+
|
|
722
|
+
@State private var name = ""
|
|
723
|
+
@State private var phone = ""
|
|
724
|
+
@State private var note = ""
|
|
725
|
+
@FocusState private var focus: Field?
|
|
726
|
+
|
|
727
|
+
var body: some View {
|
|
728
|
+
Form {
|
|
729
|
+
TextField("Name", text: $name)
|
|
730
|
+
.focused($focus, equals: .name)
|
|
731
|
+
.onSubmit { focus = .phone }
|
|
732
|
+
TextField("Phone", text: $phone)
|
|
733
|
+
.focused($focus, equals: .phone)
|
|
734
|
+
.onSubmit { focus = .note }
|
|
735
|
+
TextField("Note", text: $note)
|
|
736
|
+
.focused($focus, equals: .note)
|
|
737
|
+
}
|
|
738
|
+
.onAppear { focus = .name }
|
|
794
739
|
}
|
|
795
|
-
.onAppear { focusedField = .title }
|
|
796
|
-
}
|
|
797
740
|
}
|
|
798
741
|
```
|
|
799
742
|
|
|
800
|
-
|
|
743
|
+
A growing list of fields:
|
|
801
744
|
|
|
802
745
|
```swift
|
|
803
|
-
struct
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
746
|
+
struct SurveyAnswers: View {
|
|
747
|
+
enum Field: Hashable { case answer(Int) }
|
|
748
|
+
|
|
749
|
+
@State private var answers = [""]
|
|
750
|
+
@FocusState private var focus: Field?
|
|
751
|
+
|
|
752
|
+
var body: some View {
|
|
753
|
+
Form {
|
|
754
|
+
ForEach(answers.indices, id: \.self) { index in
|
|
755
|
+
TextField("Answer \(index + 1)", text: $answers[index])
|
|
756
|
+
.focused($focus, equals: .answer(index))
|
|
757
|
+
.onSubmit { addAnswer(after: index) }
|
|
758
|
+
}
|
|
759
|
+
}
|
|
814
760
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
focused = .option(currentIndex)
|
|
761
|
+
|
|
762
|
+
private func addAnswer(after index: Int) {
|
|
763
|
+
answers.append("")
|
|
764
|
+
let next = answers.count - 1
|
|
765
|
+
Task { @MainActor in
|
|
766
|
+
try? await Task.sleep(for: Duration.milliseconds(15))
|
|
767
|
+
focus = .answer(next)
|
|
768
|
+
}
|
|
824
769
|
}
|
|
825
|
-
}
|
|
826
770
|
}
|
|
827
771
|
```
|
|
828
772
|
|
|
829
|
-
|
|
773
|
+
The short sleep lets SwiftUI insert the new field before focus moves to it.
|
|
774
|
+
|
|
775
|
+
Guidelines:
|
|
830
776
|
|
|
831
|
-
-
|
|
832
|
-
-
|
|
833
|
-
|
|
777
|
+
- The view holding the text fields also holds their `@FocusState`.
|
|
778
|
+
- React to focus moving: show a validation message or a hint for the field
|
|
779
|
+
that just gained or lost focus.
|
|
780
|
+
- Inside a `ScrollView` or `Form`, pair focus with `.scrollDismissesKeyboard(_:)`.
|
|
834
781
|
|
|
835
|
-
|
|
782
|
+
Pitfalls:
|
|
836
783
|
|
|
837
|
-
-
|
|
838
|
-
-
|
|
784
|
+
- Focus state stored in shared objects; it is local to the view.
|
|
785
|
+
- Focus changed aggressively in the middle of an animation. Delay it when
|
|
786
|
+
needed.
|