@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,122 +1,107 @@
|
|
|
1
|
-
# TipKit Patterns
|
|
1
|
+
# TipKit Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Longer worked patterns for the `tipkit` skill. Every example targets iOS 17 or
|
|
4
|
+
later and follows Swift 6.3 conventions; a heading says so when a newer
|
|
5
|
+
release is needed.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
|
-
- [
|
|
10
|
-
- [TipView and popoverTip
|
|
11
|
-
- [
|
|
9
|
+
- [A tip with both a parameter and an event rule](#a-tip-with-both-a-parameter-and-an-event-rule)
|
|
10
|
+
- [Placing TipView and popoverTip](#placing-tipview-and-popovertip)
|
|
11
|
+
- [Counting a manual action to suggest a shortcut](#counting-a-manual-action-to-suggest-a-shortcut)
|
|
12
|
+
- [Events that carry a value](#events-that-carry-a-value)
|
|
12
13
|
- [Custom TipViewStyle](#custom-tipviewstyle)
|
|
13
|
-
- [TipGroup
|
|
14
|
-
- [Testing
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
14
|
+
- [Sequencing with TipGroup (iOS 18)](#sequencing-with-tipgroup-ios-18)
|
|
15
|
+
- [Testing](#testing)
|
|
16
|
+
- [Actions that open a feature](#actions-that-open-a-feature)
|
|
17
|
+
- [Working alongside an onboarding flow](#working-alongside-an-onboarding-flow)
|
|
18
|
+
- [A full app](#a-full-app)
|
|
18
19
|
|
|
19
|
-
##
|
|
20
|
+
## A tip with both a parameter and an event rule
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
feature discovery.
|
|
22
|
+
This tip advertises bulk export. It waits until the person is signed in and
|
|
23
|
+
has exported single recipes a few times, so the advanced feature is offered
|
|
24
|
+
only to someone who already knows the basic one.
|
|
25
25
|
|
|
26
26
|
```swift
|
|
27
27
|
import TipKit
|
|
28
28
|
|
|
29
|
-
struct
|
|
30
|
-
// Parameter rule: user must be logged in
|
|
29
|
+
struct BulkExportTip: Tip {
|
|
31
30
|
@Parameter
|
|
32
|
-
static var
|
|
31
|
+
static var isSignedIn: Bool = false
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
static let searchPerformed = Tips.Event(id: "searchPerformed")
|
|
33
|
+
static let recipeExported = Tips.Event(id: "recipeExported")
|
|
36
34
|
|
|
37
|
-
var title: Text {
|
|
38
|
-
Text("Try Advanced Search")
|
|
39
|
-
}
|
|
35
|
+
var title: Text { Text("Export a whole cookbook") }
|
|
40
36
|
|
|
41
37
|
var message: Text? {
|
|
42
|
-
Text("
|
|
38
|
+
Text("Select several recipes and share them as one file.")
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
var image: Image? {
|
|
46
|
-
Image(systemName: "magnifyingglass")
|
|
47
|
-
}
|
|
41
|
+
var image: Image? { Image(systemName: "tray.and.arrow.up") }
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#Rule(Self.$isLoggedIn) { $0 == true }
|
|
52
|
-
#Rule(Self.searchPerformed) { $0.donations.count >= 3 }
|
|
43
|
+
var options: [TipOption] {
|
|
44
|
+
Tips.MaxDisplayCount(5)
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
var
|
|
56
|
-
|
|
47
|
+
var rules: [Rule] {
|
|
48
|
+
#Rule(Self.$isSignedIn) { $0 == true }
|
|
49
|
+
#Rule(Self.recipeExported) { $0.donations.count >= 3 }
|
|
57
50
|
}
|
|
58
51
|
}
|
|
59
52
|
```
|
|
60
53
|
|
|
61
|
-
###
|
|
54
|
+
### Where to donate
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
donation
|
|
56
|
+
Donate at the exact place the action completes, for example in the export
|
|
57
|
+
callback. Every donation raises the count the rule checks.
|
|
65
58
|
|
|
66
59
|
```swift
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
var body: some View {
|
|
71
|
-
SearchBar(text: $query, onSubmit: {
|
|
72
|
-
performSearch(query)
|
|
73
|
-
// Donate each time the user searches
|
|
74
|
-
AdvancedSearchTip.searchPerformed.donate()
|
|
75
|
-
})
|
|
76
|
-
}
|
|
60
|
+
func exportRecipe(_ recipe: Recipe) async throws {
|
|
61
|
+
try await exporter.write(recipe)
|
|
62
|
+
await BulkExportTip.recipeExported.donate()
|
|
77
63
|
}
|
|
78
64
|
```
|
|
79
65
|
|
|
80
|
-
###
|
|
66
|
+
### Where to set parameters
|
|
81
67
|
|
|
82
|
-
Set parameter
|
|
83
|
-
|
|
68
|
+
Set a parameter when the underlying state changes, such as in the sign-in
|
|
69
|
+
success path. The value is stored in the TipKit datastore, so it survives
|
|
70
|
+
relaunches.
|
|
84
71
|
|
|
85
72
|
```swift
|
|
86
|
-
func
|
|
87
|
-
|
|
73
|
+
func didFinishSignIn() {
|
|
74
|
+
BulkExportTip.isSignedIn = true
|
|
88
75
|
}
|
|
89
76
|
```
|
|
90
77
|
|
|
91
|
-
## TipView and popoverTip
|
|
78
|
+
## Placing TipView and popoverTip
|
|
92
79
|
|
|
93
|
-
###
|
|
80
|
+
### A tip as a list row, the same tip as a toolbar popover
|
|
94
81
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
An inline `TipView` can be a row of a `List`; it animates away once dismissed
|
|
83
|
+
or invalidated. The toolbar button below also carries the tip as a popover and
|
|
84
|
+
invalidates it when tapped.
|
|
98
85
|
|
|
99
86
|
```swift
|
|
100
|
-
struct
|
|
101
|
-
let filterTip =
|
|
102
|
-
@State private var
|
|
87
|
+
struct PantryListView: View {
|
|
88
|
+
let filterTip = PantryFilterTip()
|
|
89
|
+
@State private var isFilterSheetShown = false
|
|
103
90
|
|
|
104
91
|
var body: some View {
|
|
105
92
|
List {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
Section {
|
|
94
|
+
TipView(filterTip)
|
|
95
|
+
}
|
|
96
|
+
ForEach(pantryItems) { item in
|
|
97
|
+
PantryRow(item: item)
|
|
110
98
|
}
|
|
111
99
|
}
|
|
112
|
-
.navigationTitle("Items")
|
|
113
100
|
.toolbar {
|
|
114
101
|
ToolbarItem(placement: .primaryAction) {
|
|
115
|
-
Button {
|
|
116
|
-
showFilters()
|
|
102
|
+
Button("Filter", systemImage: "slider.horizontal.3") {
|
|
117
103
|
filterTip.invalidate(reason: .actionPerformed)
|
|
118
|
-
|
|
119
|
-
Image(systemName: "line.3.horizontal.decrease.circle")
|
|
104
|
+
isFilterSheetShown = true
|
|
120
105
|
}
|
|
121
106
|
.popoverTip(filterTip, arrowEdge: .top)
|
|
122
107
|
}
|
|
@@ -125,658 +110,535 @@ struct ItemListView: View {
|
|
|
125
110
|
}
|
|
126
111
|
```
|
|
127
112
|
|
|
128
|
-
###
|
|
113
|
+
### Pointing at a navigation bar button
|
|
129
114
|
|
|
130
|
-
|
|
131
|
-
button, drawing the user's attention to the exact control.
|
|
115
|
+
A popover on a bar button aims its arrow at exactly that control.
|
|
132
116
|
|
|
133
117
|
```swift
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
var body: some View {
|
|
138
|
-
TextEditor(text: $text)
|
|
139
|
-
.toolbar {
|
|
140
|
-
ToolbarItem(placement: .primaryAction) {
|
|
141
|
-
Button("Undo", systemImage: "arrow.uturn.backward") {
|
|
142
|
-
undoLastAction()
|
|
143
|
-
undoTip.invalidate(reason: .actionPerformed)
|
|
144
|
-
}
|
|
145
|
-
.popoverTip(undoTip, arrowEdge: .top)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
118
|
+
Button("Revert", systemImage: "arrow.uturn.backward") {
|
|
119
|
+
revertLastChange()
|
|
149
120
|
}
|
|
121
|
+
.popoverTip(revertTip, arrowEdge: .top)
|
|
150
122
|
```
|
|
151
123
|
|
|
152
|
-
###
|
|
124
|
+
### Introducing a new tab (iOS 18)
|
|
153
125
|
|
|
154
|
-
|
|
155
|
-
|
|
126
|
+
TipKit has no `popoverTip` for `Tab`: the modifier is declared on `View`, and a
|
|
127
|
+
`Tab` is `TabContent`, so `.popoverTip` on it does not compile. Put the tip
|
|
128
|
+
inside the new tab's content instead.
|
|
156
129
|
|
|
157
130
|
```swift
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Tab("Discover", systemImage: "sparkles") {
|
|
168
|
-
DiscoverView()
|
|
169
|
-
}
|
|
170
|
-
.popoverTip(newTabTip)
|
|
131
|
+
TabView {
|
|
132
|
+
Tab("Recipes", systemImage: "book") {
|
|
133
|
+
RecipesView()
|
|
134
|
+
}
|
|
135
|
+
Tab("Meal Plan", systemImage: "calendar") {
|
|
136
|
+
VStack {
|
|
137
|
+
TipView(mealPlanTip)
|
|
138
|
+
MealPlanView()
|
|
171
139
|
}
|
|
172
140
|
}
|
|
173
141
|
}
|
|
174
142
|
```
|
|
175
143
|
|
|
176
|
-
##
|
|
144
|
+
## Counting a manual action to suggest a shortcut
|
|
177
145
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
disclosure: let users learn the basic workflow first, then reveal shortcuts.
|
|
146
|
+
A progressive-disclosure pattern: count how often someone does something the
|
|
147
|
+
slow way, and after a threshold (five here) suggest the faster path.
|
|
181
148
|
|
|
182
149
|
```swift
|
|
183
|
-
struct
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
var title: Text {
|
|
187
|
-
Text("Save Faster with Command-S")
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
var message: Text? {
|
|
191
|
-
Text("Press Command-S instead of using the menu to save your work instantly.")
|
|
192
|
-
}
|
|
150
|
+
struct SaveShortcutTip: Tip {
|
|
151
|
+
var title: Text { Text("Press Command-S to save") }
|
|
152
|
+
var message: Text? { Text("Works whenever a keyboard is attached.") }
|
|
193
153
|
|
|
194
|
-
|
|
195
|
-
Image(systemName: "keyboard")
|
|
196
|
-
}
|
|
154
|
+
static let manualSave = Tips.Event(id: "manualSave")
|
|
197
155
|
|
|
198
156
|
var rules: [Rule] {
|
|
199
|
-
|
|
200
|
-
#Rule(Self.manualSaveEvent) { $0.donations.count >= 5 }
|
|
157
|
+
#Rule(Self.manualSave) { $0.donations.count >= 5 }
|
|
201
158
|
}
|
|
202
159
|
}
|
|
203
160
|
|
|
204
|
-
struct
|
|
205
|
-
|
|
161
|
+
struct NoteEditorView: View {
|
|
162
|
+
@State private var noteText = ""
|
|
163
|
+
let keyboardHint = SaveShortcutTip()
|
|
206
164
|
|
|
207
165
|
var body: some View {
|
|
208
|
-
VStack {
|
|
209
|
-
TipView(
|
|
210
|
-
|
|
166
|
+
VStack(spacing: 0) {
|
|
167
|
+
TipView(keyboardHint)
|
|
168
|
+
TextEditor(text: $noteText)
|
|
211
169
|
}
|
|
212
170
|
.toolbar {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
KeyboardShortcutTip.manualSaveEvent.donate()
|
|
217
|
-
}
|
|
171
|
+
Button("Save") {
|
|
172
|
+
saveDraft()
|
|
173
|
+
Task { await SaveShortcutTip.manualSave.donate() }
|
|
218
174
|
}
|
|
219
175
|
}
|
|
220
176
|
}
|
|
221
177
|
}
|
|
222
178
|
```
|
|
223
179
|
|
|
224
|
-
|
|
180
|
+
## Events that carry a value
|
|
225
181
|
|
|
226
|
-
|
|
227
|
-
|
|
182
|
+
An event can record a value with every donation. Declare it as
|
|
183
|
+
`Tips.Event<Value>` where the value type is `Codable` and `Sendable`, filter
|
|
184
|
+
on the value in the rule, and pass the value to `donate`. Inside `#Rule`, give
|
|
185
|
+
`filter` its closure in parentheses: the macro rejects trailing-closure
|
|
186
|
+
syntax with "The filter function is not supported in this rule".
|
|
228
187
|
|
|
229
188
|
```swift
|
|
230
|
-
struct
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
189
|
+
struct PlaybackInfo: Codable, Sendable {
|
|
190
|
+
let tier: String
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
struct OfflineDownloadTip: Tip {
|
|
194
|
+
static let episodePlayed = Tips.Event<PlaybackInfo>(id: "episodePlayed")
|
|
235
195
|
|
|
236
|
-
|
|
196
|
+
var title: Text { Text("Download for offline listening") }
|
|
237
197
|
|
|
238
198
|
var rules: [Rule] {
|
|
239
|
-
#Rule(Self.
|
|
240
|
-
$0.donations.filter
|
|
241
|
-
$0.category == "premium"
|
|
242
|
-
}.count >= 3
|
|
199
|
+
#Rule(Self.episodePlayed) {
|
|
200
|
+
$0.donations.filter({ $0.tier == "premium" }).count >= 3
|
|
243
201
|
}
|
|
244
202
|
}
|
|
245
|
-
|
|
246
|
-
var title: Text { Text("Unlock Premium Content") }
|
|
247
203
|
}
|
|
248
204
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
205
|
+
Task {
|
|
206
|
+
await OfflineDownloadTip.episodePlayed.donate(PlaybackInfo(tier: "premium"))
|
|
207
|
+
}
|
|
253
208
|
```
|
|
254
209
|
|
|
255
210
|
## Custom TipViewStyle
|
|
256
211
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
212
|
+
`Configuration` gives the style `title`, `message`, `image` and `actions`.
|
|
213
|
+
|
|
214
|
+
### A branded card
|
|
260
215
|
|
|
261
216
|
```swift
|
|
262
|
-
struct
|
|
217
|
+
struct BrandCardTipStyle: TipViewStyle {
|
|
263
218
|
func makeBody(configuration: Configuration) -> some View {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.foregroundStyle(.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
219
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
220
|
+
HStack(alignment: .top, spacing: 12) {
|
|
221
|
+
configuration.image
|
|
222
|
+
.font(.title3)
|
|
223
|
+
.frame(width: 44, height: 44, alignment: .center)
|
|
224
|
+
.background(
|
|
225
|
+
LinearGradient(
|
|
226
|
+
colors: [.orange, .pink],
|
|
227
|
+
startPoint: .topLeading,
|
|
228
|
+
endPoint: .bottomTrailing
|
|
229
|
+
),
|
|
230
|
+
in: RoundedRectangle(cornerRadius: 12)
|
|
231
|
+
)
|
|
232
|
+
.foregroundStyle(.white)
|
|
233
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
234
|
+
configuration.title
|
|
235
|
+
.font(.title3.weight(.semibold))
|
|
236
|
+
configuration.message
|
|
237
|
+
.font(.callout)
|
|
238
|
+
.foregroundStyle(.gray)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if configuration.actions.isEmpty == false {
|
|
242
|
+
HStack(spacing: 8) {
|
|
243
|
+
ForEach(configuration.actions) { tipAction in
|
|
244
|
+
Button(action: tipAction.handler) {
|
|
245
|
+
tipAction.label()
|
|
287
246
|
}
|
|
247
|
+
.buttonStyle(.bordered)
|
|
288
248
|
}
|
|
289
|
-
.padding(.top)
|
|
290
249
|
}
|
|
291
250
|
}
|
|
292
251
|
}
|
|
293
252
|
.padding()
|
|
294
|
-
.background(.
|
|
253
|
+
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18))
|
|
295
254
|
}
|
|
296
255
|
}
|
|
297
256
|
```
|
|
298
257
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
Apply the style to individual `TipView` instances or set it as the
|
|
302
|
-
environment default.
|
|
258
|
+
Apply it to one `TipView`, or set it on a parent such as a `NavigationStack`
|
|
259
|
+
so it becomes the default for everything inside:
|
|
303
260
|
|
|
304
261
|
```swift
|
|
305
|
-
// Per view
|
|
306
|
-
TipView(myTip)
|
|
307
|
-
.tipViewStyle(BrandedTipStyle())
|
|
308
|
-
|
|
309
|
-
// Environment-wide (apply to a parent container)
|
|
310
262
|
NavigationStack {
|
|
311
|
-
|
|
263
|
+
PantryListView()
|
|
312
264
|
}
|
|
313
|
-
.tipViewStyle(
|
|
265
|
+
.tipViewStyle(BrandCardTipStyle())
|
|
314
266
|
```
|
|
315
267
|
|
|
316
|
-
###
|
|
268
|
+
### A compact variant
|
|
317
269
|
|
|
318
|
-
|
|
270
|
+
For narrow spaces like toolbars and sidebars, a single line on a tinted
|
|
271
|
+
capsule is enough.
|
|
319
272
|
|
|
320
273
|
```swift
|
|
321
|
-
struct
|
|
274
|
+
struct SlimTipStyle: TipViewStyle {
|
|
322
275
|
func makeBody(configuration: Configuration) -> some View {
|
|
323
|
-
|
|
324
|
-
configuration.image?
|
|
325
|
-
.foregroundStyle(.tint)
|
|
326
|
-
|
|
276
|
+
Label {
|
|
327
277
|
configuration.title
|
|
328
278
|
.font(.caption.bold())
|
|
279
|
+
.lineLimit(1)
|
|
280
|
+
} icon: {
|
|
281
|
+
configuration.image
|
|
329
282
|
}
|
|
330
|
-
.padding(.horizontal)
|
|
331
|
-
.padding(.vertical)
|
|
332
|
-
.background(.
|
|
283
|
+
.padding(.horizontal, 10)
|
|
284
|
+
.padding(.vertical, 6)
|
|
285
|
+
.background(Color.accentColor.opacity(0.15), in: Capsule())
|
|
333
286
|
}
|
|
334
287
|
}
|
|
335
288
|
```
|
|
336
289
|
|
|
337
|
-
## TipGroup
|
|
290
|
+
## Sequencing with TipGroup (iOS 18)
|
|
338
291
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
group becomes current.
|
|
292
|
+
A group shows only its current tip. Once that tip is dismissed or acted on,
|
|
293
|
+
the next eligible tip takes its place.
|
|
342
294
|
|
|
343
295
|
```swift
|
|
344
|
-
struct
|
|
345
|
-
var
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
struct OnboardingTipB: Tip {
|
|
351
|
-
var title: Text { Text("Browse Your Feed") }
|
|
352
|
-
var message: Text? { Text("Swipe through curated content tailored for you.") }
|
|
353
|
-
var image: Image? { Image(systemName: "rectangle.stack") }
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
struct OnboardingTipC: Tip {
|
|
357
|
-
var title: Text { Text("Customize Your Profile") }
|
|
358
|
-
var message: Text? { Text("Tap your avatar to set your name and preferences.") }
|
|
359
|
-
var image: Image? { Image(systemName: "person.crop.circle") }
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
struct HomeView: View {
|
|
363
|
-
let tipGroup = TipGroup(.ordered) {
|
|
364
|
-
OnboardingTipA()
|
|
365
|
-
OnboardingTipB()
|
|
366
|
-
OnboardingTipC()
|
|
296
|
+
struct ArticleFeedScreen: View {
|
|
297
|
+
@State private var onboarding = TipGroup(.ordered) {
|
|
298
|
+
FollowTopicTip()
|
|
299
|
+
SaveArticleTip()
|
|
300
|
+
ReaderModeTip()
|
|
367
301
|
}
|
|
368
302
|
|
|
369
303
|
var body: some View {
|
|
370
|
-
|
|
371
|
-
if let
|
|
372
|
-
TipView(
|
|
373
|
-
|
|
304
|
+
ScrollView {
|
|
305
|
+
if let tip = onboarding.currentTip {
|
|
306
|
+
TipView(tip) { _ in
|
|
307
|
+
tip.invalidate(reason: .actionPerformed)
|
|
374
308
|
}
|
|
375
309
|
}
|
|
376
|
-
|
|
377
|
-
FeedView()
|
|
310
|
+
ArticleFeed()
|
|
378
311
|
}
|
|
379
|
-
.padding()
|
|
380
312
|
}
|
|
381
313
|
}
|
|
382
314
|
```
|
|
383
315
|
|
|
384
|
-
###
|
|
316
|
+
### Popovers that move between controls
|
|
385
317
|
|
|
386
|
-
|
|
387
|
-
|
|
318
|
+
Give each control a popover for its own tip type by casting
|
|
319
|
+
`currentTip` to that type. As the group advances, the popover jumps to the next control.
|
|
388
320
|
|
|
389
321
|
```swift
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
var body: some View {
|
|
398
|
-
HStack {
|
|
399
|
-
Button("Search", systemImage: "magnifyingglass") { search() }
|
|
400
|
-
.popoverTip(group.currentTip as? SearchTip)
|
|
401
|
-
|
|
402
|
-
Button("Filter", systemImage: "line.3.horizontal.decrease") { filter() }
|
|
403
|
-
.popoverTip(group.currentTip as? FilterTip)
|
|
404
|
-
|
|
405
|
-
Button("Sort", systemImage: "arrow.up.arrow.down") { sort() }
|
|
406
|
-
.popoverTip(group.currentTip as? SortTip)
|
|
407
|
-
}
|
|
408
|
-
}
|
|
322
|
+
HStack {
|
|
323
|
+
Button("Follow", systemImage: "plus") { follow() }
|
|
324
|
+
.popoverTip(onboarding.currentTip as? FollowTopicTip)
|
|
325
|
+
Button("Save", systemImage: "bookmark") { save() }
|
|
326
|
+
.popoverTip(onboarding.currentTip as? SaveArticleTip)
|
|
409
327
|
}
|
|
410
328
|
```
|
|
411
329
|
|
|
412
|
-
## Testing
|
|
330
|
+
## Testing
|
|
413
331
|
|
|
414
|
-
###
|
|
332
|
+
### Previews
|
|
415
333
|
|
|
416
|
-
|
|
417
|
-
Use `showAllTipsForTesting()` to bypass rules.
|
|
334
|
+
Reset the store and force tips on so they render in Xcode previews:
|
|
418
335
|
|
|
419
336
|
```swift
|
|
420
337
|
#Preview {
|
|
421
|
-
|
|
422
|
-
.task {
|
|
338
|
+
PantryListView()
|
|
339
|
+
.task { @MainActor in
|
|
423
340
|
try? Tips.resetDatastore()
|
|
424
341
|
Tips.showAllTipsForTesting()
|
|
425
342
|
}
|
|
426
343
|
}
|
|
427
344
|
```
|
|
428
345
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
Show only one tip in a focused preview.
|
|
346
|
+
To look at one tip in isolation, show only that type:
|
|
432
347
|
|
|
433
348
|
```swift
|
|
434
|
-
#Preview("
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
.task {
|
|
441
|
-
try? Tips.resetDatastore()
|
|
442
|
-
Tips.showTipsForTesting([FavoriteTip.self])
|
|
443
|
-
}
|
|
349
|
+
#Preview("Filter tip only") {
|
|
350
|
+
PantryListView()
|
|
351
|
+
.task { @MainActor in
|
|
352
|
+
try? Tips.resetDatastore()
|
|
353
|
+
Tips.showTipsForTesting([PantryFilterTip.self])
|
|
354
|
+
}
|
|
444
355
|
}
|
|
445
356
|
```
|
|
446
357
|
|
|
447
|
-
### Unit
|
|
358
|
+
### Unit tests
|
|
448
359
|
|
|
449
|
-
|
|
450
|
-
|
|
360
|
+
Reset and configure in `setUp`, and reset again in `tearDown` so tests do not
|
|
361
|
+
leak state into each other. Move a tip toward eligibility by setting its
|
|
362
|
+
parameter and donating its event.
|
|
451
363
|
|
|
452
364
|
```swift
|
|
453
365
|
import XCTest
|
|
454
366
|
import TipKit
|
|
367
|
+
@testable import RecipeBox
|
|
368
|
+
|
|
369
|
+
final class BulkExportTipTests: XCTestCase {
|
|
370
|
+
private func clearTipStore() throws {
|
|
371
|
+
try Tips.resetDatastore()
|
|
372
|
+
}
|
|
455
373
|
|
|
456
|
-
final class TipRuleTests: XCTestCase {
|
|
457
374
|
override func setUp() async throws {
|
|
458
|
-
try
|
|
459
|
-
try
|
|
375
|
+
try clearTipStore()
|
|
376
|
+
try Tips.configure()
|
|
460
377
|
}
|
|
461
378
|
|
|
462
379
|
override func tearDown() async throws {
|
|
463
|
-
try
|
|
380
|
+
try clearTipStore()
|
|
464
381
|
}
|
|
465
382
|
|
|
466
|
-
func
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
AdvancedSearchTip.isLoggedIn = false
|
|
471
|
-
// Verify tip status
|
|
472
|
-
|
|
473
|
-
// Tip should become eligible after login + enough events
|
|
474
|
-
AdvancedSearchTip.isLoggedIn = true
|
|
475
|
-
for _ in 0..<3 {
|
|
476
|
-
AdvancedSearchTip.searchPerformed.donate()
|
|
383
|
+
func testSignedInUserAfterThreeExports_BecomesEligible() async {
|
|
384
|
+
BulkExportTip.isSignedIn = true
|
|
385
|
+
for _ in 1...3 {
|
|
386
|
+
await BulkExportTip.recipeExported.donate()
|
|
477
387
|
}
|
|
478
|
-
|
|
388
|
+
let tip = BulkExportTip()
|
|
389
|
+
// Assert on the tip's status here once eligibility is observable.
|
|
390
|
+
_ = tip
|
|
479
391
|
}
|
|
480
392
|
|
|
481
|
-
func
|
|
482
|
-
let tip =
|
|
393
|
+
func testInvalidatedTip_IsNoLongerEligible() async {
|
|
394
|
+
let tip = BulkExportTip()
|
|
483
395
|
tip.invalidate(reason: .actionPerformed)
|
|
484
|
-
//
|
|
396
|
+
// Assert the tip reports an invalidated status here.
|
|
485
397
|
}
|
|
486
398
|
}
|
|
487
399
|
```
|
|
488
400
|
|
|
489
|
-
|
|
401
|
+
The status assertions are left as placeholders; how you read a tip's status
|
|
402
|
+
depends on the TipKit API available to the test target.
|
|
490
403
|
|
|
491
|
-
|
|
492
|
-
|
|
404
|
+
### UI tests
|
|
405
|
+
|
|
406
|
+
Force tips on from the test by adding a launch argument before `launch()`.
|
|
407
|
+
`XCUIApplication` is main-actor isolated, so mark the test `@MainActor`:
|
|
493
408
|
|
|
494
409
|
```swift
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
app
|
|
498
|
-
app.
|
|
410
|
+
@MainActor
|
|
411
|
+
func testFilterTipAppears() {
|
|
412
|
+
let app = XCUIApplication()
|
|
413
|
+
app.launchArguments += ["--show-all-tips"]
|
|
414
|
+
app.launch()
|
|
415
|
+
}
|
|
499
416
|
```
|
|
500
417
|
|
|
418
|
+
Tests that have nothing to do with tips pass `--hide-all-tips` instead so no
|
|
419
|
+
tip covers the controls under test. The app reads both flags in `init`,
|
|
420
|
+
before configuring:
|
|
421
|
+
|
|
501
422
|
```swift
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
423
|
+
enum TipTestFlag: String {
|
|
424
|
+
case showAll = "--show-all-tips"
|
|
425
|
+
case hideAll = "--hide-all-tips"
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
func applyTipTestFlags() {
|
|
429
|
+
let arguments = ProcessInfo.processInfo.arguments
|
|
430
|
+
if arguments.contains(TipTestFlag.showAll.rawValue) {
|
|
505
431
|
Tips.showAllTipsForTesting()
|
|
506
|
-
}
|
|
507
|
-
if ProcessInfo.processInfo.arguments.contains("--hide-all-tips") {
|
|
432
|
+
} else if arguments.contains(TipTestFlag.hideAll.rawValue) {
|
|
508
433
|
Tips.hideAllTipsForTesting()
|
|
509
434
|
}
|
|
510
|
-
try? Tips.configure()
|
|
511
435
|
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### UI Testing Without Tips
|
|
515
|
-
|
|
516
|
-
Suppress all tips in UI tests that are not about tip behavior, so tips
|
|
517
|
-
do not interfere with other test flows.
|
|
518
436
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
437
|
+
init() {
|
|
438
|
+
#if DEBUG
|
|
439
|
+
applyTipTestFlags()
|
|
440
|
+
#endif
|
|
441
|
+
try? Tips.configure()
|
|
442
|
+
}
|
|
524
443
|
```
|
|
525
444
|
|
|
526
|
-
##
|
|
445
|
+
## Actions that open a feature
|
|
527
446
|
|
|
528
|
-
|
|
529
|
-
|
|
447
|
+
A primary action can take the person straight into the feature: it sets the
|
|
448
|
+
presentation state and invalidates the tip as performed. A "Not Now" style
|
|
449
|
+
secondary action closes the tip with `.tipClosed`.
|
|
530
450
|
|
|
531
451
|
```swift
|
|
532
|
-
struct
|
|
533
|
-
var title: Text {
|
|
534
|
-
Text("Try the New Editor")
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
var message: Text? {
|
|
538
|
-
Text("A faster, more powerful editing experience awaits.")
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
var image: Image? {
|
|
542
|
-
Image(systemName: "pencil.and.outline")
|
|
543
|
-
}
|
|
452
|
+
struct MealPlanTip: Tip {
|
|
453
|
+
var title: Text { Text("Plan your week") }
|
|
544
454
|
|
|
545
455
|
var actions: [Action] {
|
|
546
|
-
Action(id: "
|
|
547
|
-
Action(id: "
|
|
456
|
+
Action(id: "start-plan", title: "Start a Plan")
|
|
457
|
+
Action(id: "not-now", title: "Not Now")
|
|
548
458
|
}
|
|
549
459
|
}
|
|
550
460
|
|
|
551
|
-
struct
|
|
552
|
-
let
|
|
553
|
-
@State private var
|
|
461
|
+
struct PlannerEntryView: View {
|
|
462
|
+
let planTip = MealPlanTip()
|
|
463
|
+
@State private var isPlannerShown = false
|
|
554
464
|
|
|
555
465
|
var body: some View {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
case "later":
|
|
563
|
-
editorTip.invalidate(reason: .tipClosed)
|
|
564
|
-
default:
|
|
565
|
-
break
|
|
566
|
-
}
|
|
466
|
+
TipView(planTip) { action in
|
|
467
|
+
if action.id == "start-plan" {
|
|
468
|
+
isPlannerShown = true
|
|
469
|
+
planTip.invalidate(reason: .actionPerformed)
|
|
470
|
+
} else {
|
|
471
|
+
planTip.invalidate(reason: .tipClosed)
|
|
567
472
|
}
|
|
568
|
-
|
|
569
|
-
MainContentView()
|
|
570
473
|
}
|
|
571
|
-
.sheet(isPresented: $
|
|
572
|
-
|
|
474
|
+
.sheet(isPresented: $isPlannerShown) {
|
|
475
|
+
MealPlannerView()
|
|
573
476
|
}
|
|
574
477
|
}
|
|
575
478
|
}
|
|
576
479
|
```
|
|
577
480
|
|
|
578
|
-
##
|
|
481
|
+
## Working alongside an onboarding flow
|
|
579
482
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
483
|
+
Give both tips a `didFinishOnboarding` parameter (the has-completed-onboarding
|
|
484
|
+
flag). A welcome tip uses
|
|
485
|
+
`== false`, so it only appears for people who skipped onboarding. A discovery
|
|
486
|
+
tip uses `== true`, so it only appears once onboarding is done.
|
|
583
487
|
|
|
584
488
|
```swift
|
|
585
489
|
struct WelcomeTip: Tip {
|
|
586
|
-
|
|
587
|
-
static var hasCompletedOnboarding: Bool = false
|
|
490
|
+
var title: Text { Text("Your recipes, all in one place") }
|
|
588
491
|
|
|
589
|
-
|
|
590
|
-
var
|
|
492
|
+
@Parameter
|
|
493
|
+
static var didFinishOnboarding: Bool = false
|
|
591
494
|
|
|
592
495
|
var rules: [Rule] {
|
|
593
|
-
|
|
594
|
-
#Rule(Self.$hasCompletedOnboarding) { $0 == false }
|
|
496
|
+
#Rule(Self.$didFinishOnboarding) { $0 == false }
|
|
595
497
|
}
|
|
596
498
|
}
|
|
597
499
|
|
|
598
|
-
struct
|
|
599
|
-
|
|
600
|
-
static var hasCompletedOnboarding: Bool = false
|
|
500
|
+
struct CollectionsTip: Tip {
|
|
501
|
+
var title: Text { Text("Group recipes into collections") }
|
|
601
502
|
|
|
602
|
-
|
|
603
|
-
var
|
|
503
|
+
@Parameter
|
|
504
|
+
static var didFinishOnboarding: Bool = false
|
|
604
505
|
|
|
605
506
|
var rules: [Rule] {
|
|
606
|
-
|
|
607
|
-
#Rule(Self.$hasCompletedOnboarding) { $0 == true }
|
|
507
|
+
#Rule(Self.$didFinishOnboarding) { $0 == true }
|
|
608
508
|
}
|
|
609
509
|
}
|
|
510
|
+
```
|
|
610
511
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
var body: some View {
|
|
615
|
-
VStack {
|
|
616
|
-
// Onboarding pages...
|
|
617
|
-
|
|
618
|
-
Button("Get Started") {
|
|
619
|
-
completeOnboarding()
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
func completeOnboarding() {
|
|
625
|
-
// Invalidate welcome tips since onboarding covered the basics
|
|
626
|
-
WelcomeTip.hasCompletedOnboarding = true
|
|
627
|
-
FeatureDiscoveryTip.hasCompletedOnboarding = true
|
|
512
|
+
When onboarding finishes, flip both parameters, invalidate the welcome tip
|
|
513
|
+
explicitly, and close the sheet. The host view below does this in the
|
|
514
|
+
onboarding completion closure.
|
|
628
515
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
welcomeTip.invalidate(reason: .actionPerformed)
|
|
632
|
-
|
|
633
|
-
isPresented = false
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
struct ContentView: View {
|
|
638
|
-
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
|
639
|
-
@State private var showOnboarding = false
|
|
516
|
+
The host view remembers completion in `@AppStorage`, presents onboarding in a
|
|
517
|
+
sheet on first appearance, and shows both tips:
|
|
640
518
|
|
|
519
|
+
```swift
|
|
520
|
+
struct RecipeBoxHomeView: View {
|
|
521
|
+
@AppStorage("onboardingDone") private var onboardingDone = false
|
|
522
|
+
@State private var isOnboardingShown = false
|
|
641
523
|
let welcomeTip = WelcomeTip()
|
|
642
|
-
let
|
|
524
|
+
let collectionsTip = CollectionsTip()
|
|
643
525
|
|
|
644
526
|
var body: some View {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
CollectionGrid()
|
|
650
|
-
.popoverTip(discoveryTip)
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
.sheet(isPresented: $showOnboarding) {
|
|
654
|
-
OnboardingView(isPresented: $showOnboarding)
|
|
527
|
+
VStack {
|
|
528
|
+
TipView(welcomeTip)
|
|
529
|
+
RecipeGrid()
|
|
530
|
+
.popoverTip(collectionsTip)
|
|
655
531
|
}
|
|
656
532
|
.onAppear {
|
|
657
|
-
if
|
|
658
|
-
|
|
533
|
+
if onboardingDone == false {
|
|
534
|
+
isOnboardingShown = true
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
.sheet(isPresented: $isOnboardingShown) {
|
|
538
|
+
OnboardingView {
|
|
539
|
+
onboardingDone = true
|
|
540
|
+
WelcomeTip.didFinishOnboarding = true
|
|
541
|
+
CollectionsTip.didFinishOnboarding = true
|
|
542
|
+
welcomeTip.invalidate(reason: .actionPerformed)
|
|
543
|
+
isOnboardingShown = false
|
|
659
544
|
}
|
|
660
545
|
}
|
|
661
546
|
}
|
|
662
547
|
}
|
|
663
548
|
```
|
|
664
549
|
|
|
665
|
-
##
|
|
550
|
+
## A full app
|
|
666
551
|
|
|
667
|
-
|
|
668
|
-
|
|
552
|
+
Three tips: one with no rules, one that waits for three "item added"
|
|
553
|
+
donations, and one that waits until a collection exists.
|
|
669
554
|
|
|
670
555
|
```swift
|
|
671
556
|
import SwiftUI
|
|
672
557
|
import TipKit
|
|
673
558
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
var title: Text { Text("Search Your Library") }
|
|
678
|
-
var message: Text? { Text("Tap to find any item by name, tag, or date.") }
|
|
679
|
-
var image: Image? { Image(systemName: "magnifyingglass") }
|
|
559
|
+
struct ScanBarcodeTip: Tip {
|
|
560
|
+
var title: Text { Text("Scan to add") }
|
|
561
|
+
var image: Image? { Image(systemName: "barcode.viewfinder") }
|
|
680
562
|
}
|
|
681
563
|
|
|
682
|
-
struct
|
|
683
|
-
|
|
564
|
+
struct BatchEditTip: Tip {
|
|
565
|
+
var title: Text { Text("Edit several items at once") }
|
|
684
566
|
|
|
685
|
-
|
|
686
|
-
var message: Text? { Text("Group related items together for quick access.") }
|
|
687
|
-
var image: Image? { Image(systemName: "folder.badge.plus") }
|
|
567
|
+
static let itemAdded = Tips.Event(id: "itemAdded")
|
|
688
568
|
|
|
689
569
|
var rules: [Rule] {
|
|
690
|
-
#Rule(Self.
|
|
570
|
+
#Rule(Self.itemAdded) { $0.donations.count >= 3 }
|
|
691
571
|
}
|
|
692
572
|
}
|
|
693
573
|
|
|
694
|
-
struct
|
|
695
|
-
|
|
696
|
-
static var hasCreatedCollection: Bool = false
|
|
574
|
+
struct ShareCollectionTip: Tip {
|
|
575
|
+
var title: Text { Text("Send a collection to a friend") }
|
|
697
576
|
|
|
698
|
-
|
|
699
|
-
var
|
|
700
|
-
var image: Image? { Image(systemName: "square.and.arrow.up") }
|
|
577
|
+
@Parameter
|
|
578
|
+
static var hasCollection: Bool = false
|
|
701
579
|
|
|
702
580
|
var rules: [Rule] {
|
|
703
|
-
#Rule(Self.$
|
|
581
|
+
#Rule(Self.$hasCollection) { $0 == true }
|
|
704
582
|
}
|
|
705
583
|
}
|
|
706
584
|
|
|
707
|
-
// MARK: - App
|
|
708
|
-
|
|
709
585
|
@main
|
|
710
|
-
struct
|
|
586
|
+
struct PantryApp: App {
|
|
587
|
+
var body: some Scene {
|
|
588
|
+
WindowGroup {
|
|
589
|
+
NavigationStack {
|
|
590
|
+
PantryHomeView()
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
711
595
|
init() {
|
|
712
596
|
#if DEBUG
|
|
713
|
-
|
|
714
|
-
Tips.showAllTipsForTesting()
|
|
715
|
-
}
|
|
716
|
-
if ProcessInfo.processInfo.arguments.contains("--hide-all-tips") {
|
|
717
|
-
Tips.hideAllTipsForTesting()
|
|
718
|
-
}
|
|
597
|
+
applyTipTestFlags()
|
|
719
598
|
#endif
|
|
720
|
-
|
|
721
599
|
try? Tips.configure([
|
|
722
|
-
.
|
|
723
|
-
.
|
|
600
|
+
.datastoreLocation(.applicationDefault),
|
|
601
|
+
.displayFrequency(.daily)
|
|
724
602
|
])
|
|
725
603
|
}
|
|
726
|
-
|
|
727
|
-
var body: some Scene {
|
|
728
|
-
WindowGroup { LibraryView() }
|
|
729
|
-
}
|
|
730
604
|
}
|
|
731
605
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
let
|
|
736
|
-
let collectionTip = CollectionTip()
|
|
737
|
-
let shareTip = ShareTip()
|
|
738
|
-
|
|
739
|
-
@State private var items: [LibraryItem] = []
|
|
606
|
+
struct PantryHomeView: View {
|
|
607
|
+
let scanTip = ScanBarcodeTip()
|
|
608
|
+
let batchTip = BatchEditTip()
|
|
609
|
+
let shareTip = ShareCollectionTip()
|
|
740
610
|
|
|
741
611
|
var body: some View {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
612
|
+
List {
|
|
613
|
+
TipView(scanTip)
|
|
614
|
+
PantryItemsSection()
|
|
615
|
+
}
|
|
616
|
+
.toolbar {
|
|
617
|
+
ToolbarItem(placement: .primaryAction) {
|
|
618
|
+
Button("Edit", systemImage: "checklist") {
|
|
619
|
+
batchTip.invalidate(reason: .actionPerformed)
|
|
620
|
+
beginBatchEdit()
|
|
748
621
|
}
|
|
622
|
+
.popoverTip(batchTip)
|
|
749
623
|
}
|
|
750
|
-
.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
showSearch()
|
|
755
|
-
searchTip.invalidate(reason: .actionPerformed)
|
|
756
|
-
}
|
|
757
|
-
.popoverTip(searchTip)
|
|
624
|
+
ToolbarItem(placement: .secondaryAction) {
|
|
625
|
+
Button("Send", systemImage: "person.2") {
|
|
626
|
+
shareTip.invalidate(reason: .actionPerformed)
|
|
627
|
+
shareCollection()
|
|
758
628
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
.popoverTip(shareTip)
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
ToolbarItem(placement: .secondaryAction) {
|
|
769
|
-
Button("Add Item", systemImage: "plus") {
|
|
770
|
-
addItem()
|
|
771
|
-
CollectionTip.itemAddedEvent.donate()
|
|
772
|
-
}
|
|
629
|
+
.popoverTip(shareTip)
|
|
630
|
+
}
|
|
631
|
+
ToolbarItem(placement: .bottomBar) {
|
|
632
|
+
Button("Add Item") {
|
|
633
|
+
addItem()
|
|
634
|
+
Task { await BatchEditTip.itemAdded.donate() }
|
|
773
635
|
}
|
|
774
636
|
}
|
|
775
637
|
}
|
|
776
638
|
}
|
|
777
|
-
|
|
778
|
-
func addItem() { /* ... */ }
|
|
779
|
-
func showSearch() { /* ... */ }
|
|
780
|
-
func shareCollection() { /* ... */ }
|
|
781
639
|
}
|
|
782
640
|
```
|
|
641
|
+
|
|
642
|
+
The "Add Item" button carries no tip of its own; it only donates the event
|
|
643
|
+
that eventually makes `BatchEditTip` eligible. `applyTipTestFlags()` is the
|
|
644
|
+
DEBUG helper from the UI tests section.
|