@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,415 +1,304 @@
|
|
|
1
|
-
# WidgetKit
|
|
1
|
+
# WidgetKit: extended patterns
|
|
2
|
+
|
|
3
|
+
Companion to the `widgetkit` skill. Everything here assumes iOS 26 SDKs unless a
|
|
4
|
+
heading says otherwise.
|
|
2
5
|
|
|
3
6
|
## Contents
|
|
4
7
|
|
|
5
8
|
- [Timeline Strategies](#timeline-strategies)
|
|
6
|
-
- [
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [Multiple Widget Support
|
|
10
|
-
- [
|
|
9
|
+
- [Reloading timelines by push (iOS 26 and later)](#reloading-timelines-by-push-ios-26-and-later)
|
|
10
|
+
- [Deep Links and Widget URLs](#deep-links-and-widget-urls)
|
|
11
|
+
- [User-configurable widgets through App Intents](#user-configurable-widgets-through-app-intents)
|
|
12
|
+
- [Multiple Widget Support](#multiple-widget-support)
|
|
13
|
+
- [Previewing widgets and their snapshot state](#previewing-widgets-and-their-snapshot-state)
|
|
11
14
|
- [AccessoryWidgetBackground](#accessorywidgetbackground)
|
|
12
15
|
- [Lock Screen Accessory Widget Example](#lock-screen-accessory-widget-example)
|
|
13
16
|
- [Live Activity Full ActivityConfiguration Example](#live-activity-full-activityconfiguration-example)
|
|
14
17
|
- [Control Center Control Examples](#control-center-control-examples)
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
+
- [Laying out the expanded Dynamic Island](#laying-out-the-expanded-dynamic-island)
|
|
19
|
+
- [Live Activity alerts](#live-activity-alerts)
|
|
20
|
+
- [Live Activity Push Updates](#live-activity-push-updates)
|
|
18
21
|
- [ActivityAuthorizationInfo](#activityauthorizationinfo)
|
|
19
|
-
- [
|
|
22
|
+
- [Keeping widgets fast](#keeping-widgets-fast)
|
|
20
23
|
- [Xcode Setup](#xcode-setup)
|
|
21
|
-
- [
|
|
24
|
+
- [Smart Stack relevance](#smart-stack-relevance)
|
|
22
25
|
- [ActivityState Lifecycle](#activitystate-lifecycle)
|
|
23
26
|
- [ActivityStyle](#activitystyle)
|
|
24
27
|
- [Dismissal Policies](#dismissal-policies)
|
|
25
|
-
- [
|
|
28
|
+
- [Finding which widgets and activities are live](#finding-which-widgets-and-activities-are-live)
|
|
26
29
|
- [Design Patterns](#design-patterns)
|
|
27
|
-
- [
|
|
30
|
+
- [Further reading from Apple](#further-reading-from-apple)
|
|
28
31
|
|
|
29
32
|
## Timeline Strategies
|
|
30
33
|
|
|
31
|
-
###
|
|
32
|
-
|
|
33
|
-
Control when WidgetKit requests a new timeline after the current entries expire.
|
|
34
|
+
### Reload policies
|
|
34
35
|
|
|
35
|
-
| Policy |
|
|
36
|
+
| Policy | What happens | Pick it when |
|
|
36
37
|
|---|---|---|
|
|
37
|
-
| `.atEnd` |
|
|
38
|
-
| `.after(Date)` |
|
|
39
|
-
| `.never` | No automatic refresh
|
|
38
|
+
| `.atEnd` | The system asks for a new timeline once the last entry's date passes. This is the default. | Data changes at unpredictable times |
|
|
39
|
+
| `.after(Date)` | The system asks again after the given date | You know the schedule, such as trading hours or a booked delivery slot |
|
|
40
|
+
| `.never` | No automatic refresh; the app reloads it | Only a user action in the app changes the data |
|
|
40
41
|
|
|
41
|
-
###
|
|
42
|
+
### Plan several entries ahead
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
One timeline can carry many future entries. Projecting values forward means
|
|
45
|
+
fewer reloads.
|
|
45
46
|
|
|
46
47
|
```swift
|
|
47
|
-
func
|
|
48
|
-
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: now)!
|
|
54
|
-
let price = await StockService.shared.projectedPrice(at: entryDate, for: configuration.symbol)
|
|
55
|
-
entries.append(StockEntry(date: entryDate, symbol: configuration.symbol.name, price: price))
|
|
48
|
+
func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<ParkingEntry>) -> Void) {
|
|
49
|
+
let base = ParkingForecast.current()
|
|
50
|
+
let calendar = Calendar.current
|
|
51
|
+
let entries = (0..<6).compactMap { offset -> ParkingEntry? in
|
|
52
|
+
guard let slot = calendar.date(byAdding: .hour, value: offset, to: .now) else { return nil }
|
|
53
|
+
return ParkingEntry(date: slot, freeSpaces: base.projectedSpaces(at: slot))
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return Timeline(entries: entries, policy: .after(nextRefresh))
|
|
55
|
+
let refresh = calendar.date(byAdding: .minute, value: 360, to: .now) ?? .now
|
|
56
|
+
completion(Timeline(entries: entries, policy: .after(refresh)))
|
|
60
57
|
}
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
###
|
|
60
|
+
### Reloading from the app
|
|
64
61
|
|
|
65
62
|
```swift
|
|
66
|
-
|
|
67
|
-
WidgetCenter.shared.reloadTimelines(ofKind: "OrderStatusWidget")
|
|
68
|
-
|
|
69
|
-
// Reload all widgets
|
|
63
|
+
WidgetCenter.shared.reloadTimelines(ofKind: "ParkingWidget")
|
|
70
64
|
WidgetCenter.shared.reloadAllTimelines()
|
|
71
65
|
```
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
### Refresh Budget
|
|
77
|
-
|
|
78
|
-
Each configured widget has a daily refresh limit. Exemptions apply for:
|
|
79
|
-
- Foreground app usage
|
|
80
|
-
- Active media sessions
|
|
81
|
-
- Standard location service usage
|
|
67
|
+
The first call reloads one widget kind; the second reloads all of them. Both
|
|
68
|
+
cost budget.
|
|
82
69
|
|
|
83
|
-
|
|
70
|
+
### Budget rules
|
|
84
71
|
|
|
85
|
-
|
|
72
|
+
- Every configured widget gets its own daily allowance.
|
|
73
|
+
- Reloads are free when the app is frontmost, when it has an active media
|
|
74
|
+
session, or when it uses the standard location service.
|
|
75
|
+
- A widget run from Xcode's debugger has no refresh limit, so budget problems
|
|
76
|
+
only show up outside the debugger.
|
|
86
77
|
|
|
87
|
-
|
|
78
|
+
## Reloading timelines by push (iOS 26 and later)
|
|
88
79
|
|
|
89
|
-
|
|
80
|
+
A `WidgetPushHandler` receives a token for APNs. Send it to your server.
|
|
90
81
|
|
|
91
82
|
```swift
|
|
92
|
-
struct
|
|
83
|
+
struct ScoreboardPushHandler: WidgetPushHandler {
|
|
93
84
|
func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
|
|
94
|
-
let
|
|
95
|
-
Task {
|
|
96
|
-
try await ServerAPI.shared.register(widgetPushToken: tokenString)
|
|
97
|
-
}
|
|
85
|
+
let hex = hexString(pushInfo.token)
|
|
86
|
+
Task { await ScoreboardAPI.shared.registerWidgetToken(hex) }
|
|
98
87
|
}
|
|
99
88
|
}
|
|
100
89
|
```
|
|
101
90
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Send an APNs push with the widget's push token. The system calls your
|
|
105
|
-
`TimelineProvider.getTimeline` or `AppIntentTimelineProvider.timeline(for:in:)`
|
|
106
|
-
when the push arrives.
|
|
107
|
-
|
|
108
|
-
### ControlPushHandler
|
|
109
|
-
|
|
110
|
-
Equivalent handler for Control Center controls:
|
|
91
|
+
`hexString(_:)` is a small helper used throughout this file:
|
|
111
92
|
|
|
112
93
|
```swift
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for control in controls {
|
|
116
|
-
let tokenString = control.token.map { String(format: "%02x", $0) }.joined()
|
|
117
|
-
Task {
|
|
118
|
-
try await ServerAPI.shared.register(controlPushToken: tokenString)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
94
|
+
func hexString(_ bytes: Data) -> String {
|
|
95
|
+
bytes.reduce(into: "") { $0 += String(format: "%02x", $1) }
|
|
122
96
|
}
|
|
123
97
|
```
|
|
124
98
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
### widgetURL(_:)
|
|
99
|
+
When the server sends an APNs push addressed to that token, the system calls
|
|
100
|
+
`getTimeline` or `timeline(for:in:)` again.
|
|
128
101
|
|
|
129
|
-
|
|
102
|
+
Controls have their own handler that receives every control at once, as
|
|
103
|
+
`ControlInfo` values; each carries its token in an optional `pushInfo`:
|
|
130
104
|
|
|
131
105
|
```swift
|
|
132
|
-
struct
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Text(entry.status)
|
|
106
|
+
struct GaragePushHandler: ControlPushHandler {
|
|
107
|
+
func pushTokensDidChange(controls: [ControlInfo]) {
|
|
108
|
+
for control in controls {
|
|
109
|
+
guard let token = control.pushInfo?.token else { continue }
|
|
110
|
+
let hex = hexString(token)
|
|
111
|
+
Task { await GarageAPI.shared.registerControlToken(hex) }
|
|
139
112
|
}
|
|
140
|
-
.widgetURL(URL(string: "myapp://orders/\(entry.orderID)")!)
|
|
141
113
|
}
|
|
142
114
|
}
|
|
143
115
|
```
|
|
144
116
|
|
|
145
|
-
|
|
117
|
+
## Deep Links and Widget URLs
|
|
146
118
|
|
|
147
|
-
|
|
119
|
+
- `.widgetURL(_:)` gives the whole widget one destination.
|
|
120
|
+
- `Link(destination:)` adds separate tap targets, but only in `.systemMedium` and
|
|
121
|
+
larger. A `.systemSmall` widget supports `widgetURL` only.
|
|
148
122
|
|
|
149
123
|
```swift
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
var body: some View {
|
|
154
|
-
VStack {
|
|
155
|
-
ForEach(entry.orders) { order in
|
|
156
|
-
Link(destination: URL(string: "myapp://orders/\(order.id)")!) {
|
|
157
|
-
HStack {
|
|
158
|
-
Text(order.name)
|
|
159
|
-
Spacer()
|
|
160
|
-
Text(order.status)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
124
|
+
VStack {
|
|
125
|
+
Link(destination: URL(string: "harbor://berth/7")!) { BerthRow(number: 7) }
|
|
126
|
+
Link(destination: URL(string: "harbor://berth/9")!) { BerthRow(number: 9) }
|
|
166
127
|
}
|
|
128
|
+
.widgetURL(URL(string: "harbor://berths"))
|
|
167
129
|
```
|
|
168
130
|
|
|
169
|
-
|
|
131
|
+
The app picks the URL up on its scene content:
|
|
170
132
|
|
|
171
133
|
```swift
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
WindowGroup {
|
|
176
|
-
ContentView()
|
|
177
|
-
.onOpenURL { url in
|
|
178
|
-
DeepLinkRouter.shared.handle(url)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
134
|
+
WindowGroup {
|
|
135
|
+
HarborRootView()
|
|
136
|
+
.onOpenURL { url in router.open(url) }
|
|
182
137
|
}
|
|
183
138
|
```
|
|
184
139
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
## Intent-Driven Widget Configuration
|
|
188
|
-
|
|
189
|
-
### Defining a WidgetConfigurationIntent
|
|
140
|
+
## User-configurable widgets through App Intents
|
|
190
141
|
|
|
191
142
|
```swift
|
|
192
|
-
struct
|
|
193
|
-
static
|
|
194
|
-
static
|
|
143
|
+
struct PickStationIntent: WidgetConfigurationIntent {
|
|
144
|
+
static let title: LocalizedStringResource = "Choose Station"
|
|
145
|
+
static let description = IntentDescription("Pick the weather station to show.")
|
|
195
146
|
|
|
196
|
-
@Parameter(title: "
|
|
197
|
-
var
|
|
147
|
+
@Parameter(title: "Station")
|
|
148
|
+
var station: StationEntity
|
|
198
149
|
|
|
199
150
|
init() {}
|
|
200
|
-
|
|
201
|
-
init(category: CategoryEntity) {
|
|
202
|
-
self.category = category
|
|
203
|
-
}
|
|
151
|
+
init(station: StationEntity) { self.station = station }
|
|
204
152
|
}
|
|
205
|
-
```
|
|
206
153
|
|
|
207
|
-
|
|
154
|
+
struct StationEntity: AppEntity {
|
|
155
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Station"
|
|
156
|
+
static let defaultQuery = StationQuery()
|
|
208
157
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Category")
|
|
212
|
-
static var defaultQuery = CategoryQuery()
|
|
213
|
-
|
|
214
|
-
var id: String
|
|
215
|
-
var name: String
|
|
158
|
+
let id: String
|
|
159
|
+
let name: String
|
|
216
160
|
|
|
217
161
|
var displayRepresentation: DisplayRepresentation {
|
|
218
|
-
DisplayRepresentation(title:
|
|
162
|
+
DisplayRepresentation(title: LocalizedStringResource(stringLiteral: name))
|
|
219
163
|
}
|
|
220
164
|
}
|
|
221
165
|
|
|
222
|
-
struct
|
|
223
|
-
func entities(for identifiers: [String]) async throws -> [
|
|
224
|
-
await
|
|
166
|
+
struct StationQuery: EntityQuery {
|
|
167
|
+
func entities(for identifiers: [String]) async throws -> [StationEntity] {
|
|
168
|
+
await StationCatalog.shared.stations(matching: identifiers)
|
|
225
169
|
}
|
|
226
170
|
|
|
227
|
-
func suggestedEntities() async throws -> [
|
|
228
|
-
await
|
|
171
|
+
func suggestedEntities() async throws -> [StationEntity] {
|
|
172
|
+
await StationCatalog.shared.nearby()
|
|
229
173
|
}
|
|
230
174
|
|
|
231
|
-
func defaultResult() async ->
|
|
232
|
-
await
|
|
175
|
+
func defaultResult() async -> StationEntity? {
|
|
176
|
+
await StationCatalog.shared.nearby().first
|
|
233
177
|
}
|
|
234
178
|
}
|
|
235
179
|
```
|
|
236
180
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Provide pre-configured suggestions for the widget gallery:
|
|
181
|
+
Preconfigured gallery options come from the provider:
|
|
240
182
|
|
|
241
183
|
```swift
|
|
242
|
-
func recommendations() -> [AppIntentRecommendation<
|
|
243
|
-
|
|
244
|
-
(
|
|
245
|
-
("Work Tasks", .work),
|
|
246
|
-
]
|
|
247
|
-
return categories.map { name, entity in
|
|
248
|
-
let intent = SelectCategoryIntent(category: entity)
|
|
249
|
-
return AppIntentRecommendation(intent: intent, description: name)
|
|
184
|
+
func recommendations() -> [AppIntentRecommendation<PickStationIntent>] {
|
|
185
|
+
StationCatalog.featured.map { station in
|
|
186
|
+
AppIntentRecommendation(intent: PickStationIntent(station: station), description: station.name)
|
|
250
187
|
}
|
|
251
188
|
}
|
|
252
189
|
```
|
|
253
190
|
|
|
254
|
-
|
|
191
|
+
For intent and entity design beyond this, see the `app-intents` skill.
|
|
255
192
|
|
|
256
|
-
|
|
193
|
+
## Multiple Widget Support
|
|
257
194
|
|
|
258
|
-
|
|
259
|
-
@main
|
|
260
|
-
struct MyAppWidgets: WidgetBundle {
|
|
261
|
-
var body: some Widget {
|
|
262
|
-
OrderStatusWidget() // Home Screen widget
|
|
263
|
-
FavoritesWidget() // Configurable widget
|
|
264
|
-
StepsAccessoryWidget() // Lock Screen widget
|
|
265
|
-
DeliveryActivityWidget() // Live Activity
|
|
266
|
-
QuickActionControl() // Control Center
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Conditional Widgets
|
|
272
|
-
|
|
273
|
-
Include widgets conditionally based on platform or availability:
|
|
195
|
+
A bundle can mix every kind of widget, and it can gate newer ones by OS:
|
|
274
196
|
|
|
275
197
|
```swift
|
|
276
198
|
@main
|
|
277
|
-
struct
|
|
199
|
+
struct GardenWidgets: WidgetBundle {
|
|
278
200
|
var body: some Widget {
|
|
279
|
-
|
|
201
|
+
SoilMoistureWidget()
|
|
202
|
+
PickBedWidget()
|
|
203
|
+
SoilLockScreenWidget()
|
|
204
|
+
WateringActivityWidget()
|
|
280
205
|
if #available(iOS 18, *) {
|
|
281
|
-
|
|
206
|
+
SprinklerControl()
|
|
282
207
|
}
|
|
283
208
|
}
|
|
284
209
|
}
|
|
285
210
|
```
|
|
286
211
|
|
|
287
|
-
##
|
|
288
|
-
|
|
289
|
-
### Xcode Previews
|
|
212
|
+
## Previewing widgets and their snapshot state
|
|
290
213
|
|
|
291
214
|
```swift
|
|
292
|
-
#Preview("
|
|
293
|
-
|
|
215
|
+
#Preview("Moisture", as: .systemSmall) {
|
|
216
|
+
SoilMoistureWidget()
|
|
294
217
|
} timeline: {
|
|
295
|
-
|
|
296
|
-
|
|
218
|
+
SoilEntry(date: .now, percent: 62)
|
|
219
|
+
SoilEntry(date: .now.addingTimeInterval(3600), percent: 48)
|
|
297
220
|
}
|
|
298
221
|
|
|
299
|
-
#Preview("
|
|
300
|
-
|
|
222
|
+
#Preview("Lock Screen", as: .accessoryCircular) {
|
|
223
|
+
SoilLockScreenWidget()
|
|
301
224
|
} timeline: {
|
|
302
|
-
|
|
225
|
+
SoilEntry(date: .now, percent: 62)
|
|
303
226
|
}
|
|
304
227
|
```
|
|
305
228
|
|
|
306
|
-
|
|
229
|
+
Live Activities preview against attributes plus a list of states:
|
|
307
230
|
|
|
308
231
|
```swift
|
|
309
|
-
#Preview("
|
|
310
|
-
|
|
232
|
+
#Preview("Watering", as: .content, using: WateringAttributes.preview) {
|
|
233
|
+
WateringActivityWidget()
|
|
311
234
|
} contentStates: {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
estimatedDeliveryTime: Date()...Date().addingTimeInterval(900),
|
|
315
|
-
currentStep: .delivering
|
|
316
|
-
)
|
|
235
|
+
WateringAttributes.ContentState(zone: 1, remaining: 300)
|
|
236
|
+
WateringAttributes.ContentState(zone: 2, remaining: 60)
|
|
317
237
|
}
|
|
318
238
|
|
|
319
|
-
#Preview("
|
|
320
|
-
|
|
239
|
+
#Preview("Compact", as: .dynamicIsland(.compact), using: WateringAttributes.preview) {
|
|
240
|
+
WateringActivityWidget()
|
|
321
241
|
} contentStates: {
|
|
322
|
-
|
|
323
|
-
driverName: "Alex",
|
|
324
|
-
estimatedDeliveryTime: Date()...Date().addingTimeInterval(900),
|
|
325
|
-
currentStep: .delivering
|
|
326
|
-
)
|
|
242
|
+
WateringAttributes.ContentState(zone: 1, remaining: 300)
|
|
327
243
|
}
|
|
328
244
|
```
|
|
329
245
|
|
|
330
|
-
|
|
246
|
+
`placeholder(in:)` is synchronous, so writing `await` inside it does not
|
|
247
|
+
compile. In the snapshot method, return sample data when `context.isPreview` is
|
|
248
|
+
true and the real current state otherwise.
|
|
331
249
|
|
|
332
|
-
|
|
333
|
-
- In `getSnapshot` / `snapshot(for:in:)`, check `context.isPreview`:
|
|
334
|
-
- When `true`, return representative sample data quickly.
|
|
335
|
-
- When `false`, return the current real state.
|
|
250
|
+
## AccessoryWidgetBackground
|
|
336
251
|
|
|
337
252
|
```swift
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// CORRECT: Return static sample data
|
|
346
|
-
func placeholder(in context: Context) -> MyEntry {
|
|
347
|
-
MyEntry(date: .now, data: SampleData.placeholder)
|
|
253
|
+
ZStack {
|
|
254
|
+
AccessoryWidgetBackground()
|
|
255
|
+
VStack(spacing: 0) {
|
|
256
|
+
Image(systemName: "leaf")
|
|
257
|
+
.widgetAccentable()
|
|
258
|
+
Text("\(entry.percent)%")
|
|
259
|
+
}
|
|
348
260
|
}
|
|
349
261
|
```
|
|
350
262
|
|
|
351
|
-
|
|
263
|
+
`AccessoryWidgetBackground()` draws the standard translucent disc or panel.
|
|
264
|
+
`.widgetAccentable()` marks the views that should pick up the tint in `.accented`
|
|
265
|
+
mode.
|
|
352
266
|
|
|
353
|
-
|
|
267
|
+
Branch on the rendering mode when full color and monochrome need different views.
|
|
268
|
+
`WidgetRenderingMode` is a struct with static values, not an enum, so the switch
|
|
269
|
+
ends in a plain `default` (`@unknown default` does not compile here):
|
|
354
270
|
|
|
355
271
|
```swift
|
|
356
|
-
struct
|
|
357
|
-
|
|
272
|
+
struct SoilBadge: View {
|
|
273
|
+
@Environment(\.widgetRenderingMode) private var mode
|
|
274
|
+
let entry: SoilEntry
|
|
358
275
|
|
|
359
276
|
var body: some View {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
.widgetAccentable()
|
|
368
|
-
}
|
|
277
|
+
switch mode {
|
|
278
|
+
case .fullColor:
|
|
279
|
+
ColorfulSoilBadge(entry: entry)
|
|
280
|
+
case .vibrant, .accented:
|
|
281
|
+
MonochromeSoilBadge(entry: entry)
|
|
282
|
+
default:
|
|
283
|
+
MonochromeSoilBadge(entry: entry)
|
|
369
284
|
}
|
|
370
285
|
}
|
|
371
286
|
}
|
|
372
287
|
```
|
|
373
288
|
|
|
374
|
-
### Rendering Mode Awareness
|
|
375
|
-
|
|
376
|
-
Lock Screen widgets render in `.vibrant` or `.accented` mode. Adapt content:
|
|
377
|
-
|
|
378
|
-
```swift
|
|
379
|
-
@Environment(\.widgetRenderingMode) var renderingMode
|
|
380
|
-
|
|
381
|
-
var body: some View {
|
|
382
|
-
switch renderingMode {
|
|
383
|
-
case .fullColor:
|
|
384
|
-
ColorfulView()
|
|
385
|
-
case .vibrant, .accented:
|
|
386
|
-
MonochromeView()
|
|
387
|
-
@unknown default:
|
|
388
|
-
MonochromeView()
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
Use `.widgetAccentable()` to mark views that should receive the accent tint in
|
|
394
|
-
`.accented` rendering mode.
|
|
395
|
-
|
|
396
289
|
## Lock Screen Accessory Widget Example
|
|
397
290
|
|
|
398
|
-
A full Lock Screen widget using accessory families and `AccessoryWidgetBackground`.
|
|
399
|
-
|
|
400
291
|
```swift
|
|
401
|
-
struct
|
|
402
|
-
let kind = "StepsWidget"
|
|
292
|
+
struct SoilLockScreenWidget: Widget {
|
|
403
293
|
var body: some WidgetConfiguration {
|
|
404
|
-
StaticConfiguration(kind:
|
|
294
|
+
StaticConfiguration(kind: "SoilLockScreen", provider: SoilProvider()) { entry in
|
|
405
295
|
ZStack {
|
|
406
296
|
AccessoryWidgetBackground()
|
|
407
|
-
|
|
408
|
-
Image(systemName: "figure.walk")
|
|
409
|
-
Text("\(entry.stepCount)").font(.headline)
|
|
410
|
-
}
|
|
297
|
+
SoilBadge(entry: entry)
|
|
411
298
|
}
|
|
412
299
|
}
|
|
300
|
+
.configurationDisplayName("Soil")
|
|
301
|
+
.description("Moisture of your main bed.")
|
|
413
302
|
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
|
|
414
303
|
}
|
|
415
304
|
}
|
|
@@ -417,661 +306,467 @@ struct StepsWidget: Widget {
|
|
|
417
306
|
|
|
418
307
|
## Live Activity Full ActivityConfiguration Example
|
|
419
308
|
|
|
420
|
-
A complete `ActivityConfiguration` with Lock Screen content and the Dynamic
|
|
421
|
-
Island closures (expanded regions, compact, minimal).
|
|
422
|
-
|
|
423
309
|
```swift
|
|
424
|
-
struct
|
|
310
|
+
struct RepairActivityWidget: Widget {
|
|
425
311
|
var body: some WidgetConfiguration {
|
|
426
|
-
ActivityConfiguration(for:
|
|
312
|
+
ActivityConfiguration(for: RepairAttributes.self) { context in
|
|
427
313
|
VStack(alignment: .leading) {
|
|
428
|
-
Text(context.attributes.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
314
|
+
Text("Ticket \(context.attributes.ticketNumber)")
|
|
315
|
+
.font(.headline)
|
|
316
|
+
Text(context.state.stage.title)
|
|
317
|
+
Text(timerInterval: context.state.window, countsDown: true)
|
|
318
|
+
.font(.title2.monospacedDigit())
|
|
434
319
|
}
|
|
435
320
|
.padding()
|
|
436
321
|
} dynamicIsland: { context in
|
|
437
322
|
DynamicIsland {
|
|
438
323
|
DynamicIslandExpandedRegion(.leading) {
|
|
439
|
-
Image(systemName: "
|
|
324
|
+
Image(systemName: "wrench.and.screwdriver")
|
|
440
325
|
}
|
|
441
326
|
DynamicIslandExpandedRegion(.trailing) {
|
|
442
|
-
Text(timerInterval: context.state.
|
|
443
|
-
.
|
|
327
|
+
Text(timerInterval: context.state.window, countsDown: true)
|
|
328
|
+
.monospacedDigit()
|
|
444
329
|
}
|
|
445
330
|
DynamicIslandExpandedRegion(.center) {
|
|
446
|
-
Text(context.
|
|
331
|
+
Text(context.state.stage.title)
|
|
332
|
+
.lineLimit(1)
|
|
447
333
|
}
|
|
448
334
|
DynamicIslandExpandedRegion(.bottom) {
|
|
449
335
|
HStack {
|
|
450
|
-
ForEach(
|
|
451
|
-
|
|
452
|
-
.
|
|
336
|
+
ForEach(RepairStage.allCases, id: \.self) { stage in
|
|
337
|
+
Capsule()
|
|
338
|
+
.fill(stage <= context.state.stage ? .primary : .tertiary)
|
|
339
|
+
.frame(height: 4)
|
|
453
340
|
}
|
|
454
341
|
}
|
|
455
342
|
}
|
|
456
343
|
} compactLeading: {
|
|
457
|
-
Image(systemName: "
|
|
344
|
+
Image(systemName: "wrench.and.screwdriver")
|
|
458
345
|
} compactTrailing: {
|
|
459
|
-
Text(timerInterval: context.state.
|
|
460
|
-
.
|
|
346
|
+
Text(timerInterval: context.state.window, countsDown: true)
|
|
347
|
+
.monospacedDigit()
|
|
348
|
+
.frame(width: 44)
|
|
461
349
|
} minimal: {
|
|
462
|
-
Image(systemName: "
|
|
350
|
+
Image(systemName: "wrench")
|
|
463
351
|
}
|
|
464
352
|
}
|
|
465
353
|
}
|
|
466
354
|
}
|
|
467
355
|
```
|
|
468
356
|
|
|
357
|
+
The compact trailing timer gets a fixed width and monospaced digits so it does
|
|
358
|
+
not jitter as it counts. `RepairStage` is assumed `Comparable` and `CaseIterable`.
|
|
359
|
+
|
|
469
360
|
## Control Center Control Examples
|
|
470
361
|
|
|
471
|
-
|
|
472
|
-
current state from a value provider.
|
|
362
|
+
A toggle reads current state from a value provider and flips it through an intent:
|
|
473
363
|
|
|
474
364
|
```swift
|
|
475
|
-
|
|
476
|
-
struct OpenCameraControl: ControlWidget {
|
|
365
|
+
struct SprinklerControl: ControlWidget {
|
|
477
366
|
var body: some ControlWidgetConfiguration {
|
|
478
|
-
StaticControlConfiguration(kind: "
|
|
479
|
-
|
|
480
|
-
Label("
|
|
367
|
+
StaticControlConfiguration(kind: "SprinklerControl", provider: SprinklerStateProvider()) { isRunning in
|
|
368
|
+
ControlWidgetToggle(isOn: isRunning, action: ToggleSprinklerIntent()) {
|
|
369
|
+
Label("Sprinkler", systemImage: isRunning ? "sprinkler.and.droplets.fill" : "sprinkler")
|
|
481
370
|
}
|
|
482
371
|
}
|
|
483
|
-
.displayName("
|
|
372
|
+
.displayName("Sprinkler")
|
|
484
373
|
}
|
|
485
374
|
}
|
|
486
375
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
376
|
+
struct SprinklerStateProvider: ControlValueProvider {
|
|
377
|
+
var previewValue: Bool { false }
|
|
378
|
+
|
|
379
|
+
func currentValue() async throws -> Bool {
|
|
380
|
+
await SprinklerHub.shared.isRunning
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
struct ToggleSprinklerIntent: SetValueIntent {
|
|
385
|
+
static let title: LocalizedStringResource = "Toggle Sprinkler"
|
|
386
|
+
|
|
387
|
+
@Parameter(title: "Running")
|
|
388
|
+
var value: Bool
|
|
389
|
+
|
|
390
|
+
func perform() async throws -> some IntentResult {
|
|
391
|
+
await SprinklerHub.shared.setRunning(value)
|
|
392
|
+
return .result()
|
|
496
393
|
}
|
|
497
394
|
}
|
|
498
395
|
```
|
|
499
396
|
|
|
500
|
-
##
|
|
397
|
+
## Laying out the expanded Dynamic Island
|
|
501
398
|
|
|
502
|
-
|
|
399
|
+
A ferry crossing shows how the regions split the work:
|
|
503
400
|
|
|
504
401
|
```swift
|
|
505
402
|
DynamicIsland {
|
|
506
403
|
DynamicIslandExpandedRegion(.leading) {
|
|
507
404
|
VStack(alignment: .leading) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
Text("UA 1234")
|
|
511
|
-
.font(.caption2)
|
|
405
|
+
Text(context.attributes.fromPort).font(.headline)
|
|
406
|
+
Text(context.state.departure, style: .time).font(.caption)
|
|
512
407
|
}
|
|
513
408
|
}
|
|
514
409
|
DynamicIslandExpandedRegion(.trailing) {
|
|
515
410
|
VStack(alignment: .trailing) {
|
|
516
|
-
Text(
|
|
517
|
-
|
|
518
|
-
Text("On Time")
|
|
519
|
-
.font(.caption2)
|
|
520
|
-
.foregroundStyle(.green)
|
|
411
|
+
Text(context.attributes.toPort).font(.headline)
|
|
412
|
+
Text(context.state.arrival, style: .time).font(.caption)
|
|
521
413
|
}
|
|
522
414
|
}
|
|
523
415
|
DynamicIslandExpandedRegion(.center) {
|
|
524
|
-
Text(
|
|
525
|
-
.font(.caption)
|
|
526
|
-
.lineLimit(1)
|
|
416
|
+
Text(context.state.statusLine).lineLimit(1)
|
|
527
417
|
}
|
|
528
418
|
DynamicIslandExpandedRegion(.bottom) {
|
|
529
|
-
ProgressView(value:
|
|
530
|
-
.tint(.blue)
|
|
531
|
-
HStack {
|
|
532
|
-
Text("Departed 2:30 PM")
|
|
533
|
-
Spacer()
|
|
534
|
-
Text("Arrives 10:45 PM")
|
|
535
|
-
}
|
|
536
|
-
.font(.caption2)
|
|
537
|
-
.foregroundStyle(.secondary)
|
|
419
|
+
ProgressView(value: context.state.progress)
|
|
538
420
|
}
|
|
539
421
|
} compactLeading: {
|
|
540
|
-
Image(systemName: "
|
|
422
|
+
Image(systemName: "ferry")
|
|
541
423
|
} compactTrailing: {
|
|
542
|
-
Text(
|
|
543
|
-
.monospacedDigit()
|
|
424
|
+
Text(context.state.arrival, style: .timer).monospacedDigit()
|
|
544
425
|
} minimal: {
|
|
545
|
-
Image(systemName: "
|
|
426
|
+
Image(systemName: "ferry")
|
|
546
427
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
### Vertical Placement
|
|
550
|
-
|
|
551
|
-
Control vertical alignment within expanded regions:
|
|
552
|
-
|
|
553
|
-
```swift
|
|
554
|
-
DynamicIslandExpandedRegion(.leading) {
|
|
555
|
-
Text("Top")
|
|
556
|
-
.dynamicIsland(verticalPlacement: .belowIfTooWide)
|
|
557
|
-
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
### Content Margins
|
|
561
|
-
|
|
562
|
-
Override margins for specific Dynamic Island modes:
|
|
563
|
-
|
|
564
|
-
```swift
|
|
428
|
+
.keylineTint(.teal)
|
|
565
429
|
.contentMargins(.trailing, 20, for: .expanded)
|
|
566
430
|
.contentMargins(.bottom, 16, for: .expanded)
|
|
567
431
|
```
|
|
568
432
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
DynamicIsland { /* ... */ }
|
|
575
|
-
.keylineTint(.blue)
|
|
576
|
-
```
|
|
433
|
+
- `.dynamicIsland(verticalPlacement: .belowIfTooWide)` on a view inside an
|
|
434
|
+
expanded region lets it drop under the camera when it does not fit beside it.
|
|
435
|
+
- `.contentMargins(_:_:for:)` overrides the default margins for one presentation
|
|
436
|
+
mode.
|
|
437
|
+
- `.keylineTint(_:)` on the `DynamicIsland` colors its outline.
|
|
577
438
|
|
|
578
|
-
##
|
|
439
|
+
## Live Activity alerts
|
|
579
440
|
|
|
580
|
-
|
|
441
|
+
An update can alert the user visibly and audibly:
|
|
581
442
|
|
|
582
443
|
```swift
|
|
583
444
|
let alert = AlertConfiguration(
|
|
584
|
-
title: "
|
|
585
|
-
body: "
|
|
445
|
+
title: "Ferry boarding",
|
|
446
|
+
body: "Gate B is open.",
|
|
586
447
|
sound: .default
|
|
587
448
|
)
|
|
588
|
-
await activity.update(
|
|
449
|
+
await activity.update(content, alertConfiguration: alert)
|
|
589
450
|
```
|
|
590
451
|
|
|
591
|
-
|
|
452
|
+
For a custom sound, bundle the file with the app and pass `.named("horn.aiff")`.
|
|
592
453
|
|
|
593
|
-
|
|
594
|
-
let alert = AlertConfiguration(
|
|
595
|
-
title: "Score Update",
|
|
596
|
-
body: "Goal! The score is now 2-1.",
|
|
597
|
-
sound: .named("goal-horn.aiff")
|
|
598
|
-
)
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
Place the sound file in the app bundle. Use `.default` when no custom sound is needed.
|
|
602
|
-
|
|
603
|
-
## Push Notification Support for Live Activities
|
|
454
|
+
## Live Activity Push Updates
|
|
604
455
|
|
|
605
|
-
|
|
456
|
+
Request with `pushType: .token` and forward each token the activity reports.
|
|
457
|
+
`Activity` is a class that is not `Sendable`, so start this task from main-actor
|
|
458
|
+
code such as a view or an `@MainActor` model; in a nonisolated function Swift 6
|
|
459
|
+
rejects capturing `activity` in the `Task`:
|
|
606
460
|
|
|
607
461
|
```swift
|
|
608
|
-
let activity = try Activity.request(
|
|
609
|
-
attributes: attributes,
|
|
610
|
-
content: content,
|
|
611
|
-
pushType: .token // Enable push updates
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
// Observe token changes
|
|
615
462
|
Task {
|
|
616
|
-
for await
|
|
617
|
-
let
|
|
618
|
-
|
|
463
|
+
for await data in activity.pushTokenUpdates {
|
|
464
|
+
let hex = hexString(data)
|
|
465
|
+
await FerryAPI.shared.register(activityToken: hex, activityID: activity.id)
|
|
619
466
|
}
|
|
620
467
|
}
|
|
621
468
|
```
|
|
622
469
|
|
|
623
|
-
|
|
470
|
+
Push-to-start tokens come from the activity type:
|
|
624
471
|
|
|
625
472
|
```swift
|
|
626
|
-
// Observe the push-to-start token
|
|
627
473
|
Task {
|
|
628
|
-
for await
|
|
629
|
-
let
|
|
630
|
-
|
|
474
|
+
for await data in Activity<FerryAttributes>.pushToStartTokenUpdates {
|
|
475
|
+
let hex = hexString(data)
|
|
476
|
+
await FerryAPI.shared.register(startToken: hex)
|
|
631
477
|
}
|
|
632
478
|
}
|
|
633
479
|
```
|
|
634
480
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
```swift
|
|
638
|
-
let activity = try Activity.request(
|
|
639
|
-
attributes: attributes,
|
|
640
|
-
content: content,
|
|
641
|
-
pushType: .channel("delivery-updates")
|
|
642
|
-
)
|
|
643
|
-
```
|
|
481
|
+
From iOS 26 a broadcast channel works too: `pushType: .channel("ferry-route-12")`.
|
|
644
482
|
|
|
645
|
-
|
|
483
|
+
Update payload:
|
|
646
484
|
|
|
647
485
|
```json
|
|
648
486
|
{
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
},
|
|
660
|
-
"alert": {
|
|
661
|
-
"title": "Delivery Update",
|
|
662
|
-
"body": "Your driver is nearby!"
|
|
663
|
-
}
|
|
664
|
-
}
|
|
487
|
+
"aps": {
|
|
488
|
+
"timestamp": 1790400000,
|
|
489
|
+
"event": "update",
|
|
490
|
+
"content-state": {
|
|
491
|
+
"statusLine": "Crossing",
|
|
492
|
+
"progress": 0.4,
|
|
493
|
+
"window": { "lowerBound": 1790399400, "upperBound": 1790401800 }
|
|
494
|
+
},
|
|
495
|
+
"alert": { "title": "Halfway", "body": "Arriving in 20 minutes." }
|
|
496
|
+
}
|
|
665
497
|
}
|
|
666
498
|
```
|
|
667
499
|
|
|
668
|
-
|
|
500
|
+
`content-state` has to mirror the `ContentState` Codable layout key for key; a
|
|
501
|
+
`ClosedRange` encodes as `lowerBound` and `upperBound`. Info.plist keys on the
|
|
502
|
+
app target:
|
|
669
503
|
|
|
670
|
-
|
|
504
|
+
| Key | Effect |
|
|
505
|
+
|---|---|
|
|
506
|
+
| `NSSupportsLiveActivities` = YES | Allows Live Activities at all |
|
|
507
|
+
| `NSSupportsLiveActivitiesFrequentUpdates` = YES | Raises the push update budget for frequent updates |
|
|
671
508
|
|
|
672
|
-
|
|
673
|
-
|---|---|---|
|
|
674
|
-
| `NSSupportsLiveActivities` | `YES` | Enable Live Activities |
|
|
675
|
-
| `NSSupportsLiveActivitiesFrequentUpdates` | `YES` | Enable frequent push updates (budget increase) |
|
|
509
|
+
The `live-activities` skill goes deeper on the push contract.
|
|
676
510
|
|
|
677
511
|
## ActivityAuthorizationInfo
|
|
678
512
|
|
|
679
|
-
Check whether Live Activities are permitted before attempting to start one.
|
|
680
|
-
|
|
681
513
|
```swift
|
|
682
|
-
let
|
|
683
|
-
|
|
684
|
-
// Check permission synchronously
|
|
685
|
-
if authInfo.areActivitiesEnabled {
|
|
686
|
-
try Activity.request(attributes: attributes, content: content, pushType: .token)
|
|
687
|
-
}
|
|
514
|
+
let info = ActivityAuthorizationInfo()
|
|
515
|
+
guard info.areActivitiesEnabled else { return }
|
|
688
516
|
|
|
689
|
-
// Observe permission changes
|
|
690
517
|
Task {
|
|
691
|
-
for await enabled in
|
|
692
|
-
|
|
693
|
-
// Activities became available
|
|
694
|
-
}
|
|
518
|
+
for await enabled in info.activityEnablementUpdates {
|
|
519
|
+
await MainActor.run { settings.liveActivitiesOn = enabled }
|
|
695
520
|
}
|
|
696
521
|
}
|
|
697
522
|
|
|
698
|
-
|
|
699
|
-
if authInfo.frequentPushesEnabled {
|
|
700
|
-
// Safe to use frequent push updates
|
|
701
|
-
}
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
### Error Handling
|
|
523
|
+
let canPushOften = info.frequentPushesEnabled
|
|
705
524
|
|
|
706
|
-
```swift
|
|
707
525
|
do {
|
|
708
|
-
|
|
526
|
+
_ = try Activity.request(attributes: crossing, content: opening, pushType: .token)
|
|
709
527
|
} catch let error as ActivityAuthorizationError {
|
|
710
528
|
switch error {
|
|
711
|
-
case .denied:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
// Too many Live Activities across all apps
|
|
716
|
-
break
|
|
717
|
-
case .targetMaximumExceeded:
|
|
718
|
-
// Too many Live Activities for this app
|
|
719
|
-
break
|
|
720
|
-
default:
|
|
721
|
-
break
|
|
529
|
+
case .denied: showSettingsHint()
|
|
530
|
+
case .globalMaximumExceeded: showTryLater()
|
|
531
|
+
case .targetMaximumExceeded: endOldestActivity()
|
|
532
|
+
default: showGenericFailure()
|
|
722
533
|
}
|
|
723
534
|
}
|
|
724
535
|
```
|
|
725
536
|
|
|
726
|
-
|
|
537
|
+
- `.denied`: the user switched Live Activities off in Settings.
|
|
538
|
+
- `.globalMaximumExceeded`: too many activities are running across all apps.
|
|
539
|
+
- `.targetMaximumExceeded`: this app already runs its maximum.
|
|
727
540
|
|
|
728
|
-
|
|
541
|
+
## Keeping widgets fast
|
|
729
542
|
|
|
730
|
-
|
|
731
|
-
|
|
543
|
+
- Do the math in the provider. The view should receive values ready to draw, not
|
|
544
|
+
raw records.
|
|
545
|
+
- Extensions have tight memory caps. Avoid big images in the view, large
|
|
546
|
+
datasets in entries and deep view trees.
|
|
547
|
+
- Store small, pre-resized thumbnails in the shared container and draw them with
|
|
548
|
+
`.resizable()` and `.aspectRatio(contentMode: .fill)` rather than loading
|
|
549
|
+
full-resolution files.
|
|
732
550
|
|
|
733
|
-
|
|
734
|
-
// WRONG: Heavy computation in the widget view
|
|
735
|
-
struct MyWidgetView: View {
|
|
736
|
-
let entry: RawDataEntry
|
|
737
|
-
|
|
738
|
-
var body: some View {
|
|
739
|
-
let processed = HeavyProcessor.process(entry.rawData) // Slow
|
|
740
|
-
Text(processed.summary)
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// CORRECT: Pre-compute in the provider
|
|
745
|
-
func timeline(for configuration: Intent, in context: Context) async -> Timeline<ProcessedEntry> {
|
|
746
|
-
let raw = await DataStore.shared.fetch()
|
|
747
|
-
let processed = HeavyProcessor.process(raw)
|
|
748
|
-
let entry = ProcessedEntry(date: .now, summary: processed.summary, value: processed.value)
|
|
749
|
-
return Timeline(entries: [entry], policy: .atEnd)
|
|
750
|
-
}
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
### Memory Constraints
|
|
754
|
-
|
|
755
|
-
Widget extensions run with strict memory limits. Avoid:
|
|
756
|
-
- Loading large images directly in the widget view
|
|
757
|
-
- Storing large data sets in the entry
|
|
758
|
-
- Creating complex view hierarchies
|
|
759
|
-
|
|
760
|
-
### Image Handling
|
|
551
|
+
Sharing through an App Group:
|
|
761
552
|
|
|
762
553
|
```swift
|
|
763
|
-
//
|
|
764
|
-
|
|
554
|
+
// App side
|
|
555
|
+
let suite = UserDefaults(suiteName: "group.org.sample.garden")
|
|
556
|
+
suite?.set(62, forKey: "soilPercent")
|
|
557
|
+
WidgetCenter.shared.reloadTimelines(ofKind: "SoilMoisture")
|
|
765
558
|
|
|
766
|
-
//
|
|
767
|
-
|
|
768
|
-
.resizable()
|
|
769
|
-
.aspectRatio(contentMode: .fill)
|
|
559
|
+
// Provider side
|
|
560
|
+
let percent = UserDefaults(suiteName: "group.org.sample.garden")?.integer(forKey: "soilPercent") ?? 0
|
|
770
561
|
```
|
|
771
562
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
```swift
|
|
775
|
-
// In the main app: write data
|
|
776
|
-
let defaults = UserDefaults(suiteName: "group.com.example.myapp")
|
|
777
|
-
defaults?.set(encodedData, forKey: "widgetData")
|
|
778
|
-
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
|
|
779
|
-
|
|
780
|
-
// In the widget provider: read data
|
|
781
|
-
func timeline(for configuration: Intent, in context: Context) async -> Timeline<MyEntry> {
|
|
782
|
-
let defaults = UserDefaults(suiteName: "group.com.example.myapp")
|
|
783
|
-
let data = defaults?.data(forKey: "widgetData")
|
|
784
|
-
// Decode and build entry
|
|
785
|
-
}
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
For larger datasets, use a shared SQLite database or Core Data store in the
|
|
789
|
-
App Group container.
|
|
563
|
+
For bigger data, keep a SQLite or Core Data store inside the App Group container.
|
|
790
564
|
|
|
791
565
|
## Xcode Setup
|
|
792
566
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
567
|
+
- New target options: tick "Include Configuration App Intent" for a configurable
|
|
568
|
+
widget and "Include Live Activity" to get a Live Activity scaffold.
|
|
569
|
+
- Entitlements: `com.apple.security.application-groups` to share data between
|
|
570
|
+
targets, Push Notifications (`aps-environment`) when Live Activities update by push.
|
|
571
|
+
- Add the App Group to both targets with the same identifier, such as
|
|
572
|
+
`group.org.sample.garden`, then read it through `UserDefaults(suiteName:)` or
|
|
573
|
+
`FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)`.
|
|
574
|
+
- Debug by running the widget extension scheme, picking the "Widget" destination,
|
|
575
|
+
or using the canvas preview.
|
|
576
|
+
|
|
577
|
+
| Error | Fix |
|
|
803
578
|
|---|---|
|
|
804
|
-
|
|
|
805
|
-
|
|
|
806
|
-
|
|
807
|
-
### App Groups Configuration
|
|
808
|
-
|
|
809
|
-
1. Enable "App Groups" capability on both the main app target and the widget
|
|
810
|
-
extension target.
|
|
811
|
-
2. Create a shared group identifier (e.g., `group.com.example.myapp`).
|
|
812
|
-
3. Use `UserDefaults(suiteName:)` or `FileManager.containerURL(forSecurityApplicationGroupIdentifier:)`
|
|
813
|
-
for shared storage.
|
|
814
|
-
|
|
815
|
-
### Build Schemes
|
|
816
|
-
|
|
817
|
-
- Use the widget extension scheme to debug widget rendering.
|
|
818
|
-
- Select "Widget" as the run destination to launch the widget directly.
|
|
819
|
-
- Use "Preview" in Xcode canvas for rapid iteration.
|
|
579
|
+
| "Widget extension must include at least one widget" | Put `@main` on the `WidgetBundle` |
|
|
580
|
+
| "No such module 'WidgetKit'" | Link WidgetKit and SwiftUI in the extension target |
|
|
581
|
+
| `ActivityKit.ActivityAuthorizationError error 3` | The `NSSupportsLiveActivities = YES` key belongs in the main app target's Info.plist; setting it on the extension does nothing |
|
|
820
582
|
|
|
821
|
-
|
|
583
|
+
## Smart Stack relevance
|
|
822
584
|
|
|
823
|
-
|
|
824
|
-
// ERROR: "Widget extension must include at least one widget"
|
|
825
|
-
// FIX: Ensure @main is on the WidgetBundle, not a widget struct.
|
|
826
|
-
|
|
827
|
-
// ERROR: "No such module 'WidgetKit'"
|
|
828
|
-
// FIX: Ensure the widget extension target links WidgetKit and SwiftUI frameworks.
|
|
829
|
-
|
|
830
|
-
// ERROR: "The operation couldn't be completed. (ActivityKit.ActivityAuthorizationError error 3.)"
|
|
831
|
-
// FIX: Add NSSupportsLiveActivities = YES to the HOST APP's Info.plist (not the extension).
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
## Widget Relevance and Smart Stacks
|
|
835
|
-
|
|
836
|
-
### TimelineEntryRelevance
|
|
837
|
-
|
|
838
|
-
Score entries to surface widgets in Smart Stacks when relevant:
|
|
585
|
+
An entry can tell Smart Stacks how important it is and for how long:
|
|
839
586
|
|
|
840
587
|
```swift
|
|
841
|
-
struct
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
588
|
+
struct MatchEntry: TimelineEntry {
|
|
589
|
+
let date: Date
|
|
590
|
+
let score: String
|
|
591
|
+
let isLive: Bool
|
|
845
592
|
|
|
846
593
|
var relevance: TimelineEntryRelevance? {
|
|
847
|
-
isLive ? TimelineEntryRelevance(score:
|
|
594
|
+
isLive ? TimelineEntryRelevance(score: 90, duration: 2 * 3600) : nil
|
|
848
595
|
}
|
|
849
596
|
}
|
|
850
597
|
```
|
|
851
598
|
|
|
852
|
-
|
|
853
|
-
how long the
|
|
599
|
+
A higher score makes the stack more likely to rotate the widget up; `duration`
|
|
600
|
+
says how long the score holds.
|
|
854
601
|
|
|
855
|
-
|
|
602
|
+
Configurable widgets can also report relevance from the provider (iOS 18+). The
|
|
603
|
+
method returns a `WidgetRelevance` built from an array of
|
|
604
|
+
`WidgetRelevanceAttribute` values, each pairing one intent configuration with a
|
|
605
|
+
RelevanceKit context such as a date window. `.date(interval:kind:)` below needs
|
|
606
|
+
iOS 26; on iOS 18 the date window is `.date(from:to:)`:
|
|
856
607
|
|
|
857
608
|
```swift
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
609
|
+
import RelevanceKit
|
|
610
|
+
|
|
611
|
+
func relevance() async -> WidgetRelevance<PickTeamIntent> {
|
|
612
|
+
let fixtures = await FixtureStore.shared.upcoming()
|
|
613
|
+
let attributes = fixtures.map { fixture in
|
|
614
|
+
WidgetRelevanceAttribute(
|
|
615
|
+
configuration: PickTeamIntent(team: fixture.team),
|
|
616
|
+
context: .date(interval: fixture.window, kind: .scheduled)
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
return WidgetRelevance(attributes)
|
|
862
620
|
}
|
|
863
621
|
```
|
|
864
622
|
|
|
865
|
-
|
|
623
|
+
`.date(interval:kind:)` is the iOS 26 form; on iOS 18 use `.date(from:to:)`,
|
|
624
|
+
which iOS 26 deprecates.
|
|
866
625
|
|
|
867
|
-
|
|
626
|
+
## ActivityState Lifecycle
|
|
868
627
|
|
|
869
628
|
```swift
|
|
870
629
|
Task {
|
|
871
|
-
for await
|
|
872
|
-
switch
|
|
873
|
-
case .active:
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
case .
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
case .stale:
|
|
880
|
-
// Content is outdated; update or end
|
|
881
|
-
break
|
|
882
|
-
case .ended:
|
|
883
|
-
// Ended but may still be visible on Lock Screen
|
|
884
|
-
break
|
|
885
|
-
case .dismissed:
|
|
886
|
-
// Fully removed from UI; clean up resources
|
|
887
|
-
break
|
|
888
|
-
@unknown default:
|
|
889
|
-
break
|
|
630
|
+
for await phase in activity.activityStateUpdates {
|
|
631
|
+
switch phase {
|
|
632
|
+
case .active: break
|
|
633
|
+
case .pending: showQueuedBadge()
|
|
634
|
+
case .stale: await refreshOrEnd(activity)
|
|
635
|
+
case .ended: markFinished()
|
|
636
|
+
case .dismissed: cleanUp(activity.id)
|
|
637
|
+
@unknown default: break
|
|
890
638
|
}
|
|
891
639
|
}
|
|
892
640
|
}
|
|
893
641
|
```
|
|
894
642
|
|
|
895
|
-
|
|
643
|
+
| State | Meaning |
|
|
644
|
+
|---|---|
|
|
645
|
+
| `.active` | Running and visible |
|
|
646
|
+
| `.pending` | Requested but not on screen yet (iOS 26+) |
|
|
647
|
+
| `.stale` | Content is out of date; update it or end it |
|
|
648
|
+
| `.ended` | Finished, but may still sit on the Lock Screen |
|
|
649
|
+
| `.dismissed` | Gone from screen; release resources |
|
|
896
650
|
|
|
897
|
-
|
|
651
|
+
## ActivityStyle
|
|
898
652
|
|
|
899
653
|
```swift
|
|
900
|
-
|
|
901
|
-
let activity = try Activity.request(
|
|
902
|
-
attributes: attributes,
|
|
903
|
-
content: content,
|
|
904
|
-
pushType: .token,
|
|
905
|
-
style: .standard
|
|
906
|
-
)
|
|
907
|
-
|
|
908
|
-
// Transient: automatically dismissed after a period
|
|
909
|
-
let activity = try Activity.request(
|
|
910
|
-
attributes: attributes,
|
|
911
|
-
content: content,
|
|
912
|
-
pushType: .token,
|
|
913
|
-
style: .transient
|
|
914
|
-
)
|
|
654
|
+
_ = try Activity.request(attributes: goalAlert, content: snapshot, pushType: nil, style: .transient)
|
|
915
655
|
```
|
|
916
656
|
|
|
917
|
-
|
|
918
|
-
|
|
657
|
+
`.standard` stays until you end it. `.transient` (iOS 18+) is removed by the
|
|
658
|
+
system after a while; use it for short bursts such as a goal alert or a bus
|
|
659
|
+
arriving.
|
|
919
660
|
|
|
920
661
|
## Dismissal Policies
|
|
921
662
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
// Remove immediately
|
|
929
|
-
await activity.end(finalContent, dismissalPolicy: .immediate)
|
|
930
|
-
|
|
931
|
-
// Remove after a specific date (max 4 hours)
|
|
932
|
-
let removalDate = Date().addingTimeInterval(3600)
|
|
933
|
-
await activity.end(finalContent, dismissalPolicy: .after(removalDate))
|
|
934
|
-
```
|
|
935
|
-
|
|
936
|
-
## Querying Active Widgets and Activities
|
|
663
|
+
| Policy | Effect |
|
|
664
|
+
|---|---|
|
|
665
|
+
| `.default` | The system chooses when to remove it |
|
|
666
|
+
| `.immediate` | Removed right away |
|
|
667
|
+
| `.after(date)` | Removed at the given date, at most 4 hours after ending |
|
|
937
668
|
|
|
938
|
-
|
|
669
|
+
## Finding which widgets and activities are live
|
|
939
670
|
|
|
940
671
|
```swift
|
|
941
|
-
let
|
|
942
|
-
for
|
|
943
|
-
|
|
672
|
+
let configs = try await WidgetCenter.shared.currentConfigurations()
|
|
673
|
+
for info in configs {
|
|
674
|
+
logger.debug("\(info.kind) \(String(describing: info.family))")
|
|
944
675
|
}
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
### Current Live Activities
|
|
948
676
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
for activity in activities {
|
|
952
|
-
print("ID: \(activity.id), State: \(activity.activityState)")
|
|
677
|
+
for activity in Activity<FerryAttributes>.activities {
|
|
678
|
+
logger.debug("\(activity.id) \(String(describing: activity.activityState))")
|
|
953
679
|
}
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
### Observing New Activities
|
|
957
680
|
|
|
958
|
-
```swift
|
|
959
681
|
Task {
|
|
960
|
-
for await
|
|
961
|
-
|
|
682
|
+
for await started in Activity<FerryAttributes>.activityUpdates {
|
|
683
|
+
observe(started)
|
|
962
684
|
}
|
|
963
685
|
}
|
|
964
686
|
```
|
|
965
687
|
|
|
966
|
-
|
|
688
|
+
`currentConfigurations()` lists placed widgets with their `kind` and `family`.
|
|
689
|
+
`Activity<T>.activities` returns running activities, and `activityUpdates`
|
|
690
|
+
reports ones started later, including by push. `logger` is an `os.Logger`.
|
|
967
691
|
|
|
968
|
-
|
|
692
|
+
## Design Patterns
|
|
969
693
|
|
|
970
|
-
|
|
971
|
-
within a range. The system handles styling, accessibility, and rendering-mode
|
|
972
|
-
adaptation automatically.
|
|
694
|
+
### Gauge (iOS 16+)
|
|
973
695
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
- `.linearCapacity` - horizontal bar that fills leading to trailing. Use for
|
|
977
|
-
home screen widgets when a capacity bar fits.
|
|
696
|
+
`.accessoryCircular` draws an open ring with the value in the middle, the same
|
|
697
|
+
look as system complications. `.linearCapacity` fills from leading to trailing.
|
|
978
698
|
|
|
979
699
|
```swift
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
var body: some View {
|
|
985
|
-
Gauge(value: Double(entry.stepCount), in: 0...10000) {
|
|
986
|
-
Image(systemName: "figure.walk")
|
|
987
|
-
} currentValueLabel: {
|
|
988
|
-
Text("\(entry.stepCount)")
|
|
989
|
-
}
|
|
990
|
-
.gaugeStyle(.accessoryCircular)
|
|
991
|
-
}
|
|
700
|
+
Gauge(value: Double(entry.steps), in: 0...10000) {
|
|
701
|
+
Image(systemName: "figure.walk")
|
|
702
|
+
} currentValueLabel: {
|
|
703
|
+
Text(entry.steps, format: .number.notation(.compactName))
|
|
992
704
|
}
|
|
705
|
+
.gaugeStyle(.accessoryCircular)
|
|
993
706
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
Text("Storage")
|
|
707
|
+
Gauge(value: entry.usedBytes, in: 0...entry.totalBytes) {
|
|
708
|
+
Text("Backup")
|
|
997
709
|
} currentValueLabel: {
|
|
998
|
-
Text(
|
|
710
|
+
Text(Int64(entry.usedBytes).formatted(.byteCount(style: .file)))
|
|
999
711
|
}
|
|
1000
712
|
.gaugeStyle(.linearCapacity)
|
|
1001
713
|
```
|
|
1002
714
|
|
|
1003
|
-
###
|
|
1004
|
-
|
|
1005
|
-
`.containerBackground(_:for: .widget)` (iOS 17+) is the designated way to set
|
|
1006
|
-
widget backgrounds. Replaces older padding and background patterns. The system
|
|
1007
|
-
uses this placement to correctly render backgrounds across all widget surfaces.
|
|
715
|
+
### Container background
|
|
1008
716
|
|
|
1009
717
|
```swift
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
var body: some View {
|
|
1014
|
-
VStack(alignment: .leading) {
|
|
1015
|
-
Text(entry.orderName).font(.headline)
|
|
1016
|
-
Text(entry.status).foregroundStyle(.secondary)
|
|
1017
|
-
}
|
|
1018
|
-
.containerBackground(.fill.tertiary, for: .widget)
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
718
|
+
TideSummary(entry: entry)
|
|
719
|
+
.containerBackground(.fill.tertiary, for: .widget)
|
|
1021
720
|
```
|
|
1022
721
|
|
|
1023
|
-
|
|
722
|
+
The system knows where the widget is placed and draws the background correctly
|
|
723
|
+
on every surface, including removing it where the surface wants none.
|
|
1024
724
|
|
|
1025
|
-
|
|
1026
|
-
lack of per-element accessibility is acceptable since the entire widget surface
|
|
1027
|
-
is a single tap target.
|
|
725
|
+
### Canvas sparkline
|
|
1028
726
|
|
|
1029
727
|
```swift
|
|
1030
|
-
struct
|
|
728
|
+
struct Sparkline: View {
|
|
1031
729
|
let values: [Double]
|
|
1032
730
|
|
|
1033
731
|
var body: some View {
|
|
1034
732
|
Canvas { context, size in
|
|
1035
|
-
guard values.count
|
|
1036
|
-
let
|
|
1037
|
-
let
|
|
733
|
+
guard values.count >= 2, let low = values.min(), let high = values.max() else { return }
|
|
734
|
+
let span = max(high - low, .ulpOfOne)
|
|
735
|
+
let spacing = size.width / Double(values.count - 1)
|
|
1038
736
|
var path = Path()
|
|
1039
|
-
for (
|
|
1040
|
-
let
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
737
|
+
for (index, value) in values.enumerated() {
|
|
738
|
+
let point = CGPoint(
|
|
739
|
+
x: Double(index) * spacing,
|
|
740
|
+
y: size.height * (1 - CGFloat((value - low) / span))
|
|
741
|
+
)
|
|
742
|
+
index == 0 ? path.move(to: point) : path.addLine(to: point)
|
|
1044
743
|
}
|
|
1045
|
-
context.stroke(path, with: .color(.
|
|
744
|
+
context.stroke(path, with: .color(.green), lineWidth: 1.5)
|
|
1046
745
|
}
|
|
1047
746
|
}
|
|
1048
747
|
}
|
|
1049
748
|
```
|
|
1050
749
|
|
|
1051
|
-
###
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
- [
|
|
1067
|
-
- [
|
|
1068
|
-
- [
|
|
1069
|
-
- [
|
|
1070
|
-
- [
|
|
1071
|
-
- [
|
|
1072
|
-
- [
|
|
1073
|
-
- [
|
|
1074
|
-
- [ControlWidgetToggle](https://sosumi.ai/documentation/widgetkit/controlwidgettoggle)
|
|
1075
|
-
- [Keeping a widget up to date](https://sosumi.ai/documentation/widgetkit/keeping-a-widget-up-to-date)
|
|
1076
|
-
- [Adding StandBy and CarPlay support](https://sosumi.ai/documentation/widgetkit/adding-standby-and-carplay-support-to-your-widget)
|
|
1077
|
-
- [Optimizing for accented rendering and Liquid Glass](https://sosumi.ai/documentation/widgetkit/optimizing-your-widget-for-accented-rendering-mode-and-liquid-glass)
|
|
750
|
+
### Refresh budget in practice
|
|
751
|
+
|
|
752
|
+
- Widgets people look at often get about 40 to 70 refreshes a day, with entries
|
|
753
|
+
no closer than 5 minutes.
|
|
754
|
+
- Put as many future entries into each timeline as you can predict.
|
|
755
|
+
- Use `.after(date)` for scheduled data and `.never` for data only the user changes.
|
|
756
|
+
- Let `Text(timerInterval:countsDown:)` or a timer text style count down instead
|
|
757
|
+
of writing an entry per second.
|
|
758
|
+
|
|
759
|
+
## Further reading from Apple
|
|
760
|
+
|
|
761
|
+
- [WidgetKit](https://developer.apple.com/documentation/widgetkit)
|
|
762
|
+
- [ActivityKit](https://developer.apple.com/documentation/activitykit)
|
|
763
|
+
- [TimelineProvider](https://developer.apple.com/documentation/widgetkit/timelineprovider)
|
|
764
|
+
- [AppIntentTimelineProvider](https://developer.apple.com/documentation/widgetkit/appintenttimelineprovider)
|
|
765
|
+
- [ActivityAttributes](https://developer.apple.com/documentation/activitykit/activityattributes)
|
|
766
|
+
- [ActivityConfiguration](https://developer.apple.com/documentation/widgetkit/activityconfiguration)
|
|
767
|
+
- [DynamicIsland](https://developer.apple.com/documentation/widgetkit/dynamicisland)
|
|
768
|
+
- [ControlWidgetButton](https://developer.apple.com/documentation/widgetkit/controlwidgetbutton)
|
|
769
|
+
- [ControlWidgetToggle](https://developer.apple.com/documentation/widgetkit/controlwidgettoggle)
|
|
770
|
+
- [Keeping a widget up to date](https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date)
|
|
771
|
+
- [Adding StandBy and CarPlay support to your widget](https://developer.apple.com/documentation/widgetkit/adding-standby-and-carplay-support-to-your-widget)
|
|
772
|
+
- [Optimizing your widget for accented rendering mode and Liquid Glass](https://developer.apple.com/documentation/widgetkit/optimizing-your-widget-for-accented-rendering-mode-and-liquid-glass)
|