Implement, review, or improve WidgetKit widgets and controls. Use when building Home Screen, Lock Screen, StandBy, or CarPlay widgets with timeline providers; configurable widgets with AppIntentTimelineProvider; interactive widgets or Control Center controls with Button/Toggle wiring; WidgetKit push reloads, refresh budgets, deep links, Smart Stack relevance, Liquid Glass/accented rendering, widget extension setup, WidgetBundle, App Groups, and entitlements.
Implement, review, or improve WidgetKit widgets and controls. Use when building Home Screen, Lock Screen, StandBy, or CarPlay widgets with timeline providers; configurable widgets with AppIntentTimelineProvider; interactive widgets or Control Center controls with Button/Toggle wiring; WidgetKit push reloads, refresh budgets, deep links, Smart Stack relevance, Liquid Glass/accented rendering, widget extension setup, WidgetBundle, App Groups, and entitlements.
WidgetKit
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.
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 TimelineEntry struct with a date property and display data.
Implement a TimelineProvider (static) or AppIntentTimelineProvider (configurable).
Build the widget view using SwiftUI, adapting layout per WidgetFamily.
Declare the Widget conforming struct with a configuration and supported families.
Register all widgets in a WidgetBundle annotated with @main.
2. Integrate adjacent surfaces
Register an ActivityConfiguration in the widget bundle when the app has a
Live Activity, but keep ActivityAttributes, request/update/end, APNs
content-state, and Dynamic Island layout depth in activitykit.
Place Button, Toggle, ControlWidgetButton, and ControlWidgetToggle
in WidgetKit views or controls, but keep intent modeling, entities, queries,
Siri, Shortcuts, and Spotlight in app-intents.
3. Add a Control Center control
Reuse an AppIntent/OpenIntent for a button, or a SetValueIntent for a toggle.
Create a ControlWidgetButton or ControlWidgetToggle in the widget bundle.
Use StaticControlConfiguration or AppIntentControlConfiguration.
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 its body.
structOrderStatusWidget: Widget {
let kind: String="OrderStatusWidget"var body: someWidgetConfiguration {
StaticConfiguration(kind: kind, provider: OrderProvider()) { entry inOrderWidgetView(entry: entry)
}
.configurationDisplayName("Order Status")
.description("Track your current order.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
WidgetBundle
Use WidgetBundle to expose multiple widgets from a single extension.
@mainstructMyAppWidgets: WidgetBundle {
var body: someWidget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // ActivityConfiguration handoffQuickActionControl() // Control Center
}
}
Configuration Types
Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration
(recommended) for configurable widgets paired with AppIntentTimelineProvider.
Adapt layout per family using @Environment(\.widgetFamily):
@Environment(\.widgetFamily) var family
var body: someView {
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.
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.
WidgetKit owns control configuration, placement, kind, display name, push
handler, and extension registration. Control actions and value intents belong in
app-intents.
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 Gauge over manual arcs. Use .gaugeStyle(.accessoryCircular) for
Lock Screen circular widgets and .linearCapacity for 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 Canvas for 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.
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 WidgetPushHandler type 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, and
aps.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 WidgetCenter reloads remain the fallback path.
Control push reloads:
Register a ControlPushHandler with .pushHandler(...) on the
ControlWidgetConfiguration.
pushTokensDidChange(controls:) receives [ControlInfo]; read tokens from
each control's pushInfo.
Use apns-push-type: controls, topic suffix .push-type.controls, and
aps.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.IntentTimelineProvider is the older SiriKit Intents-based provider. Prefer
AppIntentTimelineProvider with 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 appropriate TimelineReloadPolicy values.
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. Use getTimeline or timeline(for:in:) for
async work.
Treating WidgetKit push payloads as state. Widget and control pushes are
reload signals. Persist state in shared storage or refetch it in the provider.
Registering widget pushes through User Notifications. Widget push tokens
come from WidgetKit handlers, not UNUserNotificationCenter.
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
.vibrant or .accented mode, 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.
Review Checklist
Widget extension target has App Groups entitlement matching the main app
@main is on the WidgetBundle, not on individual widgets
placeholder(in:) returns synchronously; getSnapshot/snapshot(for:in:) fast when isPreview
Timeline reload policy matches update frequency; reloadTimelines(ofKind:) only on data change
Layout adapts per WidgetFamily; accessory widgets tested in .vibrant mode
Interactive widgets use extension-available App Intents with Button/Toggle only
One .widgetURL(_:) fallback is used; Link subtargets 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
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