WidgetKit
Self-validate after edits. Run the skill-creator quick validator and Markdown lint on this skill.
Build home screen widgets, Lock Screen widgets, Control Center controls, and StandBy or CarPlay widget surfaces for iOS 26+.
Keep adjacent-framework guidance scoped to WidgetKit integration. Include
ActivityKit and App Intents only where they connect directly to WidgetKit
surfaces; hand off full lifecycle, APNs content-state, Siri/Shortcuts/Spotlight,
or entity-modeling work to sibling activitykit or app-intents skills.
See references/widgetkit-advanced.md for timeline strategies, push-based updates, Xcode setup, and advanced patterns.
Contents
- Workflow
- Widget Protocol and WidgetBundle
- Configuration Types
- TimelineProvider
- AppIntentTimelineProvider
- Widget Families
- Interactive Widgets (iOS 17+)
- ActivityConfiguration Handoff
- Control Center Widgets (iOS 18+)
- Lock Screen Widgets
- StandBy Mode
- Widget URL Handling and Deep Links
- Smart Stack Relevance
- Design Patterns
- iOS 26 Additions
- Common Mistakes
- Review Checklist
- References
Workflow
Stale widget first gate
For “updates only after opening the app,” diagnose delivery before changing timeline or CloudKit code:
- In a development acceptance lane, enable Settings → Developer → Widget Testing → WidgetKit Developer Mode. Leave it off only when explicitly testing production refresh budgets; label that lane because pushes remain opportunistic.
- Keep the host app closed, trigger one real data revision, and persist a provider timeline-wake timestamp plus durable-state pull result/hash.
- Split the failure:
- APNs non-200 → sender/topic/token/auth path.
- APNs 200, wake unchanged → WidgetKit budget/developer-mode, current token, or signed extension capability. Do not debug provider data yet.
- Wake advanced, pull failed → provider/shared-state or remote fetch path.
- Wake and pull succeeded, writer/provider hashes differ → durable-snapshot pull/apply race.
- Wake and pull succeeded, hashes match, visible widget stale → timeline/render/cache path.
- Accept only writer/provider hash parity plus a live Home Screen update
without opening the host. APNs 200 or a foreground
reloadTimelinesis not push-delivery proof.
1. Create a new widget
- Add a Widget Extension target in Xcode (File > New > Target > Widget Extension).
- Enable App Groups for shared data between the app and widget extension.
- Define a
TimelineEntrystruct with adateproperty and display data. - Implement a
TimelineProvider(static) orAppIntentTimelineProvider(configurable). - Build the widget view using SwiftUI, adapting layout per
WidgetFamily. - Declare the
Widgetconforming struct with a configuration and supported families. - Register all widgets in a
WidgetBundleannotated with@main.
2. Integrate adjacent surfaces
- Register an
ActivityConfigurationin the widget bundle when the app has a Live Activity, but keepActivityAttributes, request/update/end, APNscontent-state, and Dynamic Island layout depth inactivitykit. - Place
Button,Toggle,ControlWidgetButton, andControlWidgetTogglein WidgetKit views or controls, but keep intent modeling, entities, queries, Siri, Shortcuts, and Spotlight inapp-intents.
3. Add a Control Center control
- Reuse an
AppIntent/OpenIntentfor a button, or aSetValueIntentfor a toggle. - Create a
ControlWidgetButtonorControlWidgetTogglein the widget bundle. - Use
StaticControlConfigurationorAppIntentControlConfiguration.
4. Review existing widget code
Run through the Review Checklist at the end of this document.
Widget Protocol and WidgetBundle
Widget
Every widget conforms to the Widget protocol and returns a WidgetConfiguration
from body, including its display metadata and supported families.
WidgetBundle
Use one @main WidgetBundle to expose widgets, Live Activity configurations,
and controls from an extension.
Configuration Types
Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration
(recommended) for configurable widgets paired with AppIntentTimelineProvider.
Shared Modifiers
| Modifier | Purpose |
|---|---|
.configurationDisplayName(_:) |
Name shown in the widget gallery |
.description(_:) |
Description shown in the widget gallery |
.supportedFamilies(_:) |
Array of WidgetFamily values |
.supplementalActivityFamilies(_:) |
Live Activity sizes (.small, .medium) |
TimelineProvider
For static widgets, implement synchronous placeholder(in:), fast
getSnapshot(in:completion:), and live-data
getTimeline(in:completion:).
AppIntentTimelineProvider
For configurable widgets. Uses async/await natively. Receives user intent
configuration.
Implement placeholder(in:), async snapshot(for:in:), and async
timeline(for:in:), reading the supplied intent configuration.
Widget Families
| Family | Platform |
|---|---|
.systemSmall |
iOS, iPadOS, macOS, CarPlay (iOS 26+) |
.systemMedium |
iOS, iPadOS, macOS |
.systemLarge |
iOS, iPadOS, macOS |
.systemExtraLarge |
iPadOS only |
.accessoryCircular |
iOS, watchOS |
.accessoryRectangular |
iOS, watchOS |
.accessoryInline |
iOS, watchOS |
.accessoryCorner |
watchOS only |
Adapt layout per family using @Environment(\.widgetFamily):
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall: CompactView(entry: entry)
case .systemMedium: DetailedView(entry: entry)
case .accessoryCircular: CircularView(entry: entry)
default: FullView(entry: entry)
}
}
Interactive Widgets (iOS 17+)
Use Button and Toggle with intent types available to the widget extension or
shared code. WidgetKit owns the view placement; app-intents owns intent
modeling and behavior.
When an App Intent writes widget navigation state to App Group UserDefaults
and immediately reloads timelines, treat that as a cross-process handoff:
persist before reloadTimelines and verify the selected state survives a later
provider refresh. Do not add synchronize() by default; if a reproduced race
requires it, keep it inside the save helper and cover the later refresh.
Provider context does not identify why a timeline was requested, and one tap
can trigger multiple provider calls. If the provider also performs remote
catch-up, store a short, non-consuming local-navigation deadline and bypass
remote retries for every request inside that window; return an entry at the
deadline so catch-up resumes promptly. Never route local interaction through a
remote settle loop or fixed sleeps. An immediate filesystem copy of an App
Group preferences plist can lag live cfprefsd state, so a page mismatch is
not authoritative interaction readback.
ActivityConfiguration Handoff
WidgetKit registers Live Activity surfaces in the widget extension. Keep this
section to registration and rendering handoff; use activitykit for
ActivityAttributes, lifecycle, push updates, and full Dynamic Island patterns.
Control Center Widgets (iOS 18+)
WidgetKit owns control configuration, placement, kind, display name, push
handler, and extension registration. Control actions and value intents belong in
app-intents.
Lock Screen Widgets
Use accessory families and AccessoryWidgetBackground; test their rendering
without relying on full color.
StandBy Mode
Small system widgets can appear in StandBy and CarPlay. Use
@Environment(\.widgetLocation) for conditional rendering:
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.
Widget URL Handling and Deep Links
Use one .widgetURL(_:) as the whole-widget fallback route. Use Link for
deliberate subtargets only where the family and layout support them, including
.accessoryRectangular, .systemSmall, and larger system widgets. For small
widgets, prefer one clear fallback; avoid multiple Link targets unless the
visual affordance and hit areas remain unambiguous.
Never attach multiple widgetURL modifiers in the hierarchy.
Smart Stack Relevance
Use TimelineEntryRelevance(score:duration:) on timeline entries for timely
iPhone and iPad Smart Stack relevance. Keep scores on a consistent positive
scale; zero or lower means not relevant.
For configurable widgets, donate App Intents that correspond to user actions or
widget parameters from app-side code, such as with intent.donate() or
IntentDonationManager. Keep AppEntity and EntityQuery design in
app-intents.
On watchOS, contextual relevance uses
WidgetRelevance([WidgetRelevanceAttribute(...)]) from the provider
relevance() callback. That path is not used by iPhone or iPad Smart Stacks.
Design Patterns
- Prefer
Gaugeover manual arcs. Use.gaugeStyle(.accessoryCircular)for Lock Screen circular widgets and.linearCapacityfor home screen capacity bars. The system handles styling, accessibility, and rendering-mode adaptation. - Use
.containerBackground(_:for: .widget)(iOS 17+) for widget backgrounds instead of padding and background modifiers. - Use
Canvasfor dense visualizations like sparklines or mini bar charts. The lack of per-element accessibility is acceptable since the entire widget surface is a single tap target. - Match timeline refresh to data granularity. The budget is dynamic and
opportunistic; schedule useful future entries, avoid unnecessary reloads,
and use
Text(timerInterval:countsDown:)for live countdowns. Load the advanced reference for current budget guidance. - Traffic-light / brand color that must survive glass. WidgetKit vibrancy and
accent remapping will gray or retint SwiftUI
Circle/Shapefills and SF Symbol tints. Rasterize status dots (or similar chrominance-critical glyphs) toCGImage→Imagewith.renderingMode(.original), then.widgetAccentedRenderingMode(.fullColor). Encode state in shape as well as color (color-blind safe). Do not rely onColorfills alone under Liquid Glass /.vibrant/.accented.
See references/widgetkit-advanced.md for code examples and detailed guidance on each pattern.
iOS 26 Additions
Liquid Glass Support
Adapt widgets to Liquid Glass with @Environment(\.widgetRenderingMode),
.widgetAccentable(), and Image.widgetAccentedRenderingMode(_:). In
.vibrant, the system maps content into the material style, so avoid relying on
original colors alone.
Push Reload Handlers
Widget push reloads:
- Add Push Notifications capability to the widget extension target.
- Keep the
WidgetPushHandlertype in the widget extension target or shared code linked into it, not only in the main app target. - Register the handler with
.pushHandler(...)on the widget configuration. - Do not use User Notifications registration to obtain widget push tokens;
WidgetKit supplies tokens through
pushTokenDidChange(_:widgets:). - Use
apns-push-type: widgets, topic suffix.push-type.widgets, andaps.content-changed. - Treat push as a budgeted, opportunistic reload signal, not state delivery and
not the only freshness model. Timelines, reload policies, shared storage or
refetch, and app-triggered
WidgetCenterreloads remain the fallback path. - On token register / timeline wake, refetch or rewrite App Group state in
the provider. APNs
200only means Apple accepted the reload signal. - Prefer a single known CloudKit record (atomic blob / snapshot) for the widget pull path when the host is the primary writer — zone-wide scans often miss WidgetKit’s time budget and leave App Group stale.
- After the durable write succeeds, confirm before APNs: save snapshot → read-back → content hash must match → local App Group hash must still match (retry if the feed moved). Fixed sleeps and widget-side double-pulls are symptom patches for waking against an unconfirmed blob.
- When signing with an Auth Key (.p8), reuse the provider JWT for ~50
minutes. Minting a fresh JWT per send triggers APNs
TooManyProviderTokenUpdates(HTTP 429).
Control push reloads:
- Register a
ControlPushHandlerwith.pushHandler(...)on theControlWidgetConfiguration. pushTokensDidChange(controls:)receives[ControlInfo]; read tokens from each control'spushInfo.- Use
apns-push-type: controls, topic suffix.push-type.controls, andaps.content-changed.
CarPlay Widgets
Small system widgets can appear in CarPlay on iOS 26+. Ensure layouts are legible at a glance; taps and controls depend on vehicle touch support and, for opening the app, CarPlay integration.
Common Mistakes
Using IntentTimelineProvider instead of AppIntentTimelineProvider.
IntentTimelineProvideris the older SiriKit Intents-based provider. PreferAppIntentTimelineProviderwith the App Intents framework for new widgets.Exceeding the refresh budget. Widgets have a daily refresh limit. Do not call
WidgetCenter.shared.reloadTimelines(ofKind:)on every minor data change. Batch updates and use appropriateTimelineReloadPolicyvalues.Forgetting App Groups for shared data. The widget extension runs in a separate process. Use
UserDefaults(suiteName:)or a shared App Group container for data the widget reads.Performing network calls in placeholder().
placeholder(in:)must return synchronously with sample data. UsegetTimelineortimeline(for:in:)for async work.Shipping sample data from getTimeline.
placeholder/getSnapshotmay use sample data for gallery previews.getTimeline/timeline(for:in:)must read live shared state when the widget is meant to show current data.Fighting freshness with animation or polling. Prefer live relative age (
Text(.relative)/ date styles) and timeline entries at semantic boundaries over continuous symbol effects or minute-polling reloads. DeterminateGauge/ProgressViewonly when the shared model has a real fraction; never invent percent-complete for indeterminate work.Status-secondary chrome. For multi-item triage widgets, lead with consequence (needs attention / running / stale), then project and phase. Hard-coded gradient heroes and plan fractions as the primary line usually fail glanceability and break system tint / vibrant appearances — prefer adaptive
containerBackgroundand glyphs whose shape remains readable without color. When brand or traffic-light color must stay true, rasterize and apply.widgetAccentedRenderingMode(.fullColor)(see Design Patterns).Treating WidgetKit push payloads as state. Widget and control pushes are reload signals. Persist state in shared storage or refetch it in the provider. Host Refresh succeeding while the Home Screen stays stale usually means the host wrote App Group +
reloadTimelines, but the push→provider refetch path did not.Registering widget pushes through User Notifications. Widget push tokens come from WidgetKit handlers, not
UNUserNotificationCenter.Minting a new APNs provider JWT on every widgets push. Auth Key JWTs must be cached (~50 min). Uncached minting yields
429 TooManyProviderTokenUpdatesand silent Home Screen lag despite a valid.p8.Calling APNs 200 “in sync.” Run the stale-widget first gate and prove parity on the Home Screen itself. Partial row lag is still fail. In development, check WidgetKit Developer Mode before patching payloads or data code; it bypasses test budgets but is not production-budget evidence.
Zone-scanning the private DB inside
getTimelineas the only pull. Prefer onerecord(for:)snapshot the host updates on publish; keep zone-change as fallback. Do not throttle-stamp a failed pull so the next wake cannot retry.Putting heavy logic in the widget view. Widget views are rendered in a size-limited process. Pre-compute data in the timeline provider and pass display-ready values through the entry.
Ignoring accessory rendering modes. Lock Screen widgets render in
.vibrantor.accentedmode, not.fullColor. Test with@Environment(\.widgetRenderingMode)and avoid relying on color alone.Not testing on device. StandBy, CarPlay, and accessory rendering differ significantly from Simulator. Always verify on physical hardware.
Trusting SwiftUI shape fills for status color under glass. Home Screen Liquid Glass remaps fills; a green/yellow/red
Circle().fillcan wash out. Use raster +.fullColorand a shape channel when chrominance is semantic.Announcing decorative status glyphs twice. If adjacent text or the combined row owns the accessible status, hide the decorative icon from accessibility so stale or duplicate announcements cannot disagree.
Review Checklist
- Widget extension target has App Groups entitlement matching the main app
-
@mainis on theWidgetBundle, not on individual widgets -
placeholder(in:)returns synchronously;getSnapshot/snapshot(for:in:)is fast whenisPreview - Timeline reload policy matches update frequency;
reloadTimelines(ofKind:)only on data change - Layout adapts per
WidgetFamily; accessory widgets tested in.vibrantmode - Interactive widgets use extension-available App Intents with
Button/Toggleonly - One
.widgetURL(_:)fallback is used;Linksubtargets are family-appropriate - Widget push handlers live in the widget extension/shared code and do not use User Notifications token registration
- Widget/control pushes supplement timelines and shared-state/refetch fallbacks
- Durable state confirmed before push (snapshot save → read-back hash → local hash still matches); APNs 200 is not Home Screen parity
- Development push acceptance records Developer Mode state plus provider wake/pull evidence with the host closed
- APNs Auth Key JWT is reused (~50 min); push only when snapshot hash changes
- Smart Stack relevance uses timeline relevance and app-side intent donations where useful
- Live Activity lifecycle and App Intent modeling are handed off to sibling skills
- Controls use
StaticControlConfiguration/AppIntentControlConfiguration - Timeline entries and Intent types are Sendable; tested on device
References
- Advanced guide: references/widgetkit-advanced.md
- Apple docs: WidgetKit | Keeping a widget up to date | Smart Stack visibility