iOS WidgetKit Development
Treat a widget as a cross-process native system, not as an app view. Prove each
boundary independently and require direct native evidence for user-visible
behavior.
Load the Relevant Context
- Read the repository instructions and current widget architecture.
- Inspect the installed package source and generated native output for the
exact dependency version. Do not rely on remembered Expo or WidgetKit
behavior.
- Read references/generated-source-ownership.md
before changing generated Swift, config plugins, Xcode target membership, or
a dependency patch.
- Read references/appintent-configuration.md
for configurable widgets, AppEnums, DynamicOptionsProvider, or configuration
values that appear stale or ignored.
- Read references/expo-widgets.md when the project
uses Expo,
expo-widgets, serialized JavaScript layouts, or prebuild.
- Read references/live-activity-images.md
when a Live Activity or Dynamic Island image is missing, blank, monochrome,
gray, tinted, blurry, incorrectly scaled, or present in source but wrong at
runtime.
- Read
references/compact-dynamic-island-layout.md
when the compact Dynamic Island is excessively wide, has an unexplained
center gap, loses a live timer, or behaves differently after rebuilds.
- Invoke
expo-native-module-bridges when a product-owned Expo Function or
AsyncFunction carries JavaScript arguments into WidgetKit or ActivityKit.
- Read references/validation-ladder.md before
native verification, interaction measurement, or declaring the work done.
- Invoke
ios-simulator-automation for app-to-SpringBoard or Widget Gallery
automation. Keep this skill focused on WidgetKit architecture and evidence.
Map the Runtime Pipeline
Write down the actual pipeline before editing:
canonical config/source
-> generator or config plugin
-> generated Swift and Xcode target membership
-> built host app and .appex
-> AppIntent metadata and WidgetKit descriptor
-> configured timeline provider
-> timeline/shared storage
-> render bridge
-> SwiftUI widget
-> SpringBoard interaction and reload
For each stage, record its owner, module/target, input, output, and one observable
probe. If the suspected failure boundary is unknown, add probes before fixes.
Classify the Change
| Change |
Minimum rebuild action |
| App-side data or serialized layout only |
Run the new bundle and republish; rebuild only if the installed runtime cannot load it |
| Generated Swift, config plugin, entitlements, extension plist, target membership, or dependency native source |
Prebuild if applicable, rebuild, and reinstall the exact .app |
| AppIntent parameter/schema or widget kind |
Rebuild/reinstall, then remove and add a fresh widget |
| Documentation or tests only |
Reuse the last artifact unless source inputs changed |
Do not use repeated rebuilds as a substitute for identifying the failing layer.
Implement or Diagnose
1. Establish source and target ownership
- Identify the canonical source of every generated file.
- Treat generated native output as disposable evidence, not the editing source.
- Keep app-specific source out of vendor patches.
- Patch dependency source or its generator only for dependency-owned behavior.
- Verify each AppIntent/configuration type is compiled in the intended module
and each generated source is present in exactly the intended target.
- Inspect the generated project after prebuild; plugin tests alone do not prove
target membership.
2. Validate transport and storage contracts
- Keep widget payloads bounded and property-list safe.
- Reject
null, NaN, Infinity, non-string dictionary keys, and unsupported
bridged objects before publishing to shared defaults.
- Run
scripts/validate-widget-payload.py against representative and maximum
payloads when JSON props cross into UserDefaults or App Group storage.
- Exercise empty, maximum, interrupted-write, previous-version, and rollback
states when storage is custom or patched.
- Separate maximum-size fixtures from distinct-text interaction fixtures.
3. Trace configuration end to end
For every configurable parameter, distinguish these checkpoints:
- The picker exposes the intended options and default.
- SpringBoard persists the selected value.
- The timeline provider receives and decodes the selected value.
- The render bridge receives the same value.
- A fresh widget renders visibly different output.
Do not substitute metadata, registration, shared data, or unit tests for a later
checkpoint. Instrument the first mismatching boundary and make the smallest fix
that changes its evidence.
4. Keep render code extension-safe
- For a blank or red widget, inspect boundaries in runtime order: layout
registration, timeline write/readback, serialized evaluation, native node
decoding, then SwiftUI layout. Stop at the first failing boundary; do not rank
a later-layer clue above an unverified earlier layer.
- Assume the widget runs in a separate process with a constrained lifecycle.
- Keep serialized render functions self-contained when a framework evaluates
their source in another JavaScript context.
- Avoid module closures, unsupported globals, non-finite layout values, and
modifiers the installed native decoder cannot consume.
- Test the serialized form, not only direct function invocation.
- Provide a valid placeholder/snapshot/empty state independently of app launch.
5. Implement interactions according to WidgetKit semantics
- Verify whether returning from the AppIntent already reloads the timeline
before calling
WidgetCenter.reloadTimelines explicitly.
- Avoid duplicate reload requests and large unchanged storage rewrites.
- Use invalidatable content or an equivalent supported feedback mechanism while
asynchronous replacement completes.
- Measure tap-to-feedback and tap-to-new-content separately.
- Treat system scheduling latency as distinct from application persistence cost.
6. Build and inspect the exact artifact
- Build once per native-source revision and record the exact
.app path.
- Run
scripts/inspect-widget-artifact.py --app <path> to inventory the host,
embedded extensions, entitlements, extension points, and AppIntent metadata.
- Confirm host and extension App Groups, extension embedding, widget kind, and
generated metadata from the built artifact rather than source files alone.
- Install and verify that same artifact. Do not silently switch artifacts during
acceptance testing.
7. Verify the native behavior
- Start from a fresh widget after configuration schema changes.
- Use a fixture whose expected change is visually and semantically distinct.
- Observe the complete configured state and every interaction/reset state.
- Capture accessibility or state evidence in addition to screenshots.
- Preserve logs from the provider/render boundary when configuration is the
suspected failure.
- Follow the bounded recovery and cleanup rules from
ios-simulator-automation.
Apply Evidence Gates
Classify evidence explicitly:
- Build: the intended source compiled into the intended artifact.
- Registration: WidgetKit discovered and launched the extension.
- Configuration: the provider received the selected value.
- Render: the fresh widget displayed the expected state.
- Interaction: the control changed state and persisted/reset as specified.
- Performance: measured latency changed on the exact tested artifact.
Passing one category never implies another. Keep a core behavior unverified if
its native state was not observed, even when tests, metadata, and builds pass.
Manual deferral changes the delivery decision, not the acceptance truth; do not
label or archive the core behavior as implemented solely because verification
was deferred.
Keep the Diagnosis Narrow
- State each hypothesis with the evidence it predicts.
- Add one probe or make one minimal change at a time.
- Revert disproven diagnostic work before trying another layer.
- Stop broad architectural changes when a lower-level trace can discriminate
the hypotheses.
- Preserve project-owned uncommitted changes and generated-state evidence.
Handoff
Use the evidence contract in
references/validation-ladder.md. Report the
canonical source changed, generated outputs inspected, exact artifact, fresh
widget status, observed and unobserved checkpoints, measured performance, and
cleanup. Never report “probably passed.”
1---2name: ios-widgetkit-development3description: Implement, debug, review, and verify iOS WidgetKit extensions, ActivityKit Live Activities, and AppIntent-configurable widgets, including Expo or expo-widgets generated targets. Use for new widget features, blank or red widgets, missing or incorrectly rendered Live Activity images, excessively wide or empty compact Dynamic Island layouts, missing live timers, stale or ignored Edit Widget configuration, timeline and shared-storage failures, interactive Button/AppIntent behavior, generated Swift ownership, target membership, rebuild and fresh-widget decisions, performance diagnosis, or native acceptance evidence.4---56# iOS WidgetKit Development78Treat a widget as a cross-process native system, not as an app view. Prove each9boundary independently and require direct native evidence for user-visible10behavior.1112## Load the Relevant Context13141. Read the repository instructions and current widget architecture.152. Inspect the installed package source and generated native output for the16 exact dependency version. Do not rely on remembered Expo or WidgetKit17 behavior.183. Read [references/generated-source-ownership.md](references/generated-source-ownership.md)19 before changing generated Swift, config plugins, Xcode target membership, or20 a dependency patch.214. Read [references/appintent-configuration.md](references/appintent-configuration.md)22 for configurable widgets, AppEnums, DynamicOptionsProvider, or configuration23 values that appear stale or ignored.245. Read [references/expo-widgets.md](references/expo-widgets.md) when the project25 uses Expo, `expo-widgets`, serialized JavaScript layouts, or prebuild.266. Read [references/live-activity-images.md](references/live-activity-images.md)27 when a Live Activity or Dynamic Island image is missing, blank, monochrome,28 gray, tinted, blurry, incorrectly scaled, or present in source but wrong at29 runtime.307. Read31 [references/compact-dynamic-island-layout.md](references/compact-dynamic-island-layout.md)32 when the compact Dynamic Island is excessively wide, has an unexplained33 center gap, loses a live timer, or behaves differently after rebuilds.348. Invoke `expo-native-module-bridges` when a product-owned Expo `Function` or35 `AsyncFunction` carries JavaScript arguments into WidgetKit or ActivityKit.369. Read [references/validation-ladder.md](references/validation-ladder.md) before37 native verification, interaction measurement, or declaring the work done.3810. Invoke `ios-simulator-automation` for app-to-SpringBoard or Widget Gallery39 automation. Keep this skill focused on WidgetKit architecture and evidence.4041## Map the Runtime Pipeline4243Write down the actual pipeline before editing:4445```text46canonical config/source47 -> generator or config plugin48 -> generated Swift and Xcode target membership49 -> built host app and .appex50 -> AppIntent metadata and WidgetKit descriptor51 -> configured timeline provider52 -> timeline/shared storage53 -> render bridge54 -> SwiftUI widget55 -> SpringBoard interaction and reload56```5758For each stage, record its owner, module/target, input, output, and one observable59probe. If the suspected failure boundary is unknown, add probes before fixes.6061## Classify the Change6263| Change | Minimum rebuild action |64| --- | --- |65| App-side data or serialized layout only | Run the new bundle and republish; rebuild only if the installed runtime cannot load it |66| Generated Swift, config plugin, entitlements, extension plist, target membership, or dependency native source | Prebuild if applicable, rebuild, and reinstall the exact `.app` |67| AppIntent parameter/schema or widget kind | Rebuild/reinstall, then remove and add a fresh widget |68| Documentation or tests only | Reuse the last artifact unless source inputs changed |6970Do not use repeated rebuilds as a substitute for identifying the failing layer.7172## Implement or Diagnose7374### 1. Establish source and target ownership7576- Identify the canonical source of every generated file.77- Treat generated native output as disposable evidence, not the editing source.78- Keep app-specific source out of vendor patches.79- Patch dependency source or its generator only for dependency-owned behavior.80- Verify each AppIntent/configuration type is compiled in the intended module81 and each generated source is present in exactly the intended target.82- Inspect the generated project after prebuild; plugin tests alone do not prove83 target membership.8485### 2. Validate transport and storage contracts8687- Keep widget payloads bounded and property-list safe.88- Reject `null`, `NaN`, `Infinity`, non-string dictionary keys, and unsupported89 bridged objects before publishing to shared defaults.90- Run `scripts/validate-widget-payload.py` against representative and maximum91 payloads when JSON props cross into `UserDefaults` or App Group storage.92- Exercise empty, maximum, interrupted-write, previous-version, and rollback93 states when storage is custom or patched.94- Separate maximum-size fixtures from distinct-text interaction fixtures.9596### 3. Trace configuration end to end9798For every configurable parameter, distinguish these checkpoints:991001. The picker exposes the intended options and default.1012. SpringBoard persists the selected value.1023. The timeline provider receives and decodes the selected value.1034. The render bridge receives the same value.1045. A fresh widget renders visibly different output.105106Do not substitute metadata, registration, shared data, or unit tests for a later107checkpoint. Instrument the first mismatching boundary and make the smallest fix108that changes its evidence.109110### 4. Keep render code extension-safe111112- For a blank or red widget, inspect boundaries in runtime order: layout113 registration, timeline write/readback, serialized evaluation, native node114 decoding, then SwiftUI layout. Stop at the first failing boundary; do not rank115 a later-layer clue above an unverified earlier layer.116- Assume the widget runs in a separate process with a constrained lifecycle.117- Keep serialized render functions self-contained when a framework evaluates118 their source in another JavaScript context.119- Avoid module closures, unsupported globals, non-finite layout values, and120 modifiers the installed native decoder cannot consume.121- Test the serialized form, not only direct function invocation.122- Provide a valid placeholder/snapshot/empty state independently of app launch.123124### 5. Implement interactions according to WidgetKit semantics125126- Verify whether returning from the AppIntent already reloads the timeline127 before calling `WidgetCenter.reloadTimelines` explicitly.128- Avoid duplicate reload requests and large unchanged storage rewrites.129- Use invalidatable content or an equivalent supported feedback mechanism while130 asynchronous replacement completes.131- Measure tap-to-feedback and tap-to-new-content separately.132- Treat system scheduling latency as distinct from application persistence cost.133134### 6. Build and inspect the exact artifact135136- Build once per native-source revision and record the exact `.app` path.137- Run `scripts/inspect-widget-artifact.py --app <path>` to inventory the host,138 embedded extensions, entitlements, extension points, and AppIntent metadata.139- Confirm host and extension App Groups, extension embedding, widget kind, and140 generated metadata from the built artifact rather than source files alone.141- Install and verify that same artifact. Do not silently switch artifacts during142 acceptance testing.143144### 7. Verify the native behavior145146- Start from a fresh widget after configuration schema changes.147- Use a fixture whose expected change is visually and semantically distinct.148- Observe the complete configured state and every interaction/reset state.149- Capture accessibility or state evidence in addition to screenshots.150- Preserve logs from the provider/render boundary when configuration is the151 suspected failure.152- Follow the bounded recovery and cleanup rules from153 `ios-simulator-automation`.154155## Apply Evidence Gates156157Classify evidence explicitly:158159- **Build:** the intended source compiled into the intended artifact.160- **Registration:** WidgetKit discovered and launched the extension.161- **Configuration:** the provider received the selected value.162- **Render:** the fresh widget displayed the expected state.163- **Interaction:** the control changed state and persisted/reset as specified.164- **Performance:** measured latency changed on the exact tested artifact.165166Passing one category never implies another. Keep a core behavior unverified if167its native state was not observed, even when tests, metadata, and builds pass.168Manual deferral changes the delivery decision, not the acceptance truth; do not169label or archive the core behavior as implemented solely because verification170was deferred.171172## Keep the Diagnosis Narrow173174- State each hypothesis with the evidence it predicts.175- Add one probe or make one minimal change at a time.176- Revert disproven diagnostic work before trying another layer.177- Stop broad architectural changes when a lower-level trace can discriminate178 the hypotheses.179- Preserve project-owned uncommitted changes and generated-state evidence.180181## Handoff182183Use the evidence contract in184[references/validation-ladder.md](references/validation-ladder.md). Report the185canonical source changed, generated outputs inspected, exact artifact, fresh186widget status, observed and unobserved checkpoints, measured performance, and187cleanup. Never report “probably passed.”