Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 109% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 209% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 113% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 113% | 0% |
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.
TimelineEntry struct with a date property and display data.TimelineProvider (static) or AppIntentTimelineProvider (configurable).WidgetFamily.Widget conforming struct with a configuration and supported families.WidgetBundle annotated with @main.ActivityConfiguration in the widget bundle when the app has aLive Activity, but keep ActivityAttributes, request/update/end, APNs content-state, and Dynamic Island layout depth in activitykit.
Button, Toggle, ControlWidgetButton, and ControlWidgetTogglein WidgetKit views or controls, but keep intent modeling, entities, queries, Siri, Shortcuts, and Spotlight in app-intents.
AppIntent/OpenIntent for a button, or a SetValueIntent for a toggle.ControlWidgetButton or ControlWidgetToggle in the widget bundle.StaticControlConfiguration or AppIntentControlConfiguration.Run through the Review Checklist at the end of this document.
Every widget conforms to the Widget protocol and returns a WidgetConfiguration from its body.
swiftstruct OrderStatusWidget: Widget { let kind: String = "OrderStatusWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: OrderProvider()) { entry in OrderWidgetView(entry: entry) } .configurationDisplayName("Order Status") .description("Track your current order.") .supportedFamilies([.systemSmall, .systemMedium]) } }
Use WidgetBundle to expose multiple widgets from a single extension.
swift@main struct MyAppWidgets: WidgetBundle { var body: some Widget { OrderStatusWidget() FavoritesWidget() DeliveryActivityWidget() // ActivityConfiguration handoff QuickActionControl() // Control Center } }
Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration (recommended) for configurable widgets paired with AppIntentTimelineProvider.
swift// Static StaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in MyWidgetView(entry: entry) } // Configurable AppIntentConfiguration(kind: "ConfigWidget", intent: SelectCategoryIntent.self, provider: CategoryProvider()) { entry in CategoryWidgetView(entry: entry) }
| 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) |
For static (non-configurable) widgets. Uses completion handlers. Three required methods:
swiftstruct WeatherProvider: TimelineProvider { typealias Entry = WeatherEntry func placeholder(in context: Context) -> WeatherEntry { WeatherEntry(date: .now, temperature: 72, condition: "Sunny") } func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) { let entry = context.isPreview ? placeholder(in: context) : WeatherEntry(date: .now, temperature: currentTemp, condition: currentCondition) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) { Task { let weather = await WeatherService.shared.fetch() let entry = WeatherEntry(date: .now, temperature: weather.temp, condition: weather.condition) let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: .now)! completion(Timeline(entries: [entry], policy: .after(nextUpdate))) } } }
For configurable widgets. Uses async/await natively. Receives user intent configuration.
swiftstruct CategoryProvider: AppIntentTimelineProvider { typealias Entry = CategoryEntry typealias Intent = SelectCategoryIntent func placeholder(in context: Context) -> CategoryEntry { CategoryEntry(date: .now, categoryName: "Sample", items: []) } func snapshot(for config: SelectCategoryIntent, in context: Context) async -> CategoryEntry { let items = await DataStore.shared.items(for: config.category) return CategoryEntry(date: .now, categoryName: config.category.name, items: items) } func timeline(for config: SelectCategoryIntent, in context: Context) async -> Timeline<CategoryEntry> { let items = await DataStore.shared.items(for: config.category) let entry = CategoryEntry(date: .now, categoryName: config.category.name, items: items) return Timeline(entries: [entry], policy: .atEnd) } }
| 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):
swift@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) } }
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.
swiftstruct InteractiveWidgetView: View { let entry: FavoriteEntry var body: some View { Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) { Image(systemName: entry.isFavorite ? "star.fill" : "star") } } }
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.
swiftstruct DeliveryActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: DeliveryAttributes.self) { context in DeliveryLiveActivityView(context: context) } dynamicIsland: { context in DeliveryDynamicIsland(context: context) } } }
WidgetKit owns control configuration, placement, kind, display name, push handler, and extension registration. Control actions and value intents belong in app-intents.
swiftstruct OpenCameraControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "OpenCamera") { ControlWidgetButton(action: OpenCameraIntent()) { Label("Camera", systemImage: "camera.fill") } } .displayName("Open Camera") } } struct FlashlightControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "Flashlight", provider: FlashlightValueProvider()) { value in ControlWidgetToggle(isOn: value, action: ToggleFlashlightIntent()) { Label("Flashlight", systemImage: value ? "flashlight.on.fill" : "flashlight.off.fill") } } .displayName("Flashlight") } }
Use accessory families and AccessoryWidgetBackground.
swiftstruct StepsWidget: Widget { let kind = "StepsWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: StepsProvider()) { entry in ZStack { AccessoryWidgetBackground() VStack { Image(systemName: "figure.walk") Text("\(entry.stepCount)").font(.headline) } } } .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline]) } }
Small system widgets can appear in StandBy and CarPlay. Use @Environment(\.widgetLocation) for conditional rendering:
swift@Environment(\.widgetLocation) var location // location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.
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.
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.
Gauge over manual arcs. Use .gaugeStyle(.accessoryCircular) forLock Screen circular widgets and .linearCapacity for home screen capacity bars. The system handles styling, accessibility, and rendering-mode adaptation.
.containerBackground(_:for: .widget) (iOS 17+) for widget backgroundsinstead of padding and background modifiers.
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.
Text(timerInterval:countsDown:) for live countdowns. Load the advanced reference for current budget guidance.See references/widgetkit-advanced.md for code examples and detailed guidance on each pattern.
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.
Widget push reloads:
WidgetPushHandler type in the widget extension target or sharedcode linked into it, not only in the main app target.
.pushHandler(...) on the widget configuration.WidgetKit supplies tokens through pushTokenDidChange(_:widgets:).
apns-push-type: widgets, topic suffix .push-type.widgets, andaps.content-changed.
not the only freshness model. Timelines, reload policies, shared storage or refetch, and app-triggered WidgetCenter reloads remain the fallback path.
Control push reloads:
ControlPushHandler with .pushHandler(...) on theControlWidgetConfiguration.
pushTokensDidChange(controls:) receives [ControlInfo]; read tokens fromeach control's pushInfo.
apns-push-type: controls, topic suffix .push-type.controls, andaps.content-changed.
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.
IntentTimelineProvider is the older SiriKit Intents-based provider. Prefer AppIntentTimelineProvider with the App Intents framework for new widgets.
call WidgetCenter.shared.reloadTimelines(ofKind:) on every minor data change. Batch updates and use appropriate TimelineReloadPolicy values.
separate process. Use UserDefaults(suiteName:) or a shared App Group container for data the widget reads.
placeholder(in:) must returnsynchronously with sample data. Use getTimeline or timeline(for:in:) for async work.
reload signals. Persist state in shared storage or refetch it in the provider.
come from WidgetKit handlers, not UNUserNotificationCenter.
size-limited process. Pre-compute data in the timeline provider and pass display-ready values through the entry.
.vibrant or .accented mode, not .fullColor. Test with @Environment(\.widgetRenderingMode) and avoid relying on color alone.
significantly from Simulator. Always verify on physical hardware.
@main is on the WidgetBundle, not on individual widgetsplaceholder(in:) returns synchronously; getSnapshot/snapshot(for:in:) fast when isPreviewreloadTimelines(ofKind:) only on data changeWidgetFamily; accessory widgets tested in .vibrant modeButton/Toggle only.widgetURL(_:) fallback is used; Link subtargets are family-appropriateStaticControlConfiguration/AppIntentControlConfigurationOther measured skills in the registry, with their headline benchmark lift.