widgetkit
dpearson2699/swift-ios-skills
Build Home Screen, Lock Screen, Control Center, and StandBy widgets for iOS with timeline providers and interactive controls.
What is widgetkit?
WidgetKit enables you to create widgets and controls that appear on iOS Home Screen, Lock Screen, Control Center, StandBy, and CarPlay surfaces. Use this skill when implementing timeline-based widgets, configurable widgets with AppIntent, interactive widgets with Button/Toggle controls, push-based updates, and widget extension setup.
- Create static and configurable widgets using TimelineProvider and AppIntentTimelineProvider
- Build interactive widgets and Control Center controls with Button and Toggle wiring
- Support multiple widget families (systemSmall, systemMedium, systemLarge, accessoryCircular, accessoryRectangular, accessoryInline)
- Implement Lock Screen, StandBy, and CarPlay widget surfaces
- Handle widget push reloads, refresh budgets, and deep link navigation
- Configure Smart Stack relevance and Liquid Glass/accented rendering
How to install widgetkit
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill widgetkit- Xcode with Widget Extension target template
- App Groups entitlement enabled for shared data between app and widget extension
- iOS 16+ (iOS 18+ for Control Center controls, iOS 26+ for CarPlay)
How to use widgetkit
- 1.Create a new Widget Extension target in Xcode
- 2.Define a TimelineEntry struct with a date property and display data
- 3.Implement TimelineProvider for static widgets or AppIntentTimelineProvider for configurable widgets
- 4.Build the widget view using SwiftUI, adapting layout per WidgetFamily
- 5.Create a Widget struct conforming to Widget protocol with WidgetConfiguration
- 6.Register all widgets in a WidgetBundle annotated with @main
- 7.Enable App Groups for data sharing between app and extension
- 8.Configure supported families and add display name and description
Use cases
- Display real-time order status or delivery tracking on Home Screen
- Show weather, calendar, or news updates in Lock Screen widgets
- Create quick-action toggles in Control Center (iOS 18+)
- Build interactive widgets that respond to user taps without opening the app
- Implement Live Activity handoff from ActivityKit to widget surfaces
- iOS app developers building widget extensions
- Engineers implementing Home Screen or Lock Screen experiences
- Developers creating Control Center quick actions
- Teams integrating Live Activities with widget surfaces
widgetkit FAQ
TimelineProvider is for static, non-configurable widgets and uses completion handlers. AppIntentTimelineProvider is for configurable widgets that accept user intent configuration and uses async/await natively.
Use Button and Toggle views in your widget SwiftUI layout (iOS 17+). Wire them to AppIntent actions. For Control Center, use ControlWidgetButton or ControlWidgetToggle with StaticControlConfiguration or AppIntentControlConfiguration.
Use App Groups to share data between the app and widget extension. The widget's timeline provider can read from the shared container and refresh based on your update policy.
Start with systemSmall and systemMedium for Home Screen. Add accessoryCircular, accessoryRectangular, or accessoryInline for Lock Screen. Use systemLarge for detailed views. CarPlay and StandBy have specific family requirements.
Use .widgetURL() modifier on your widget view to set a URL, or use Button with an AppIntent that opens a URL. Parse the URL in your app's SceneDelegate or using onOpenURL modifier.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: widgetkit description: "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.
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
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 its body.
struct 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])
}
}
WidgetBundle
Use WidgetBundle to expose multiple widgets from a single extension.
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // ActivityConfiguration handoff
QuickActionControl() // Control Center
}
}
Configuration Types
Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration
(recommended) for configurable widgets paired with AppIntentTimelineProvider.
// 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)
}
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 (non-configurable) widgets. Uses completion handlers. Three required methods:
struct 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)))
}
}
}
AppIntentTimelineProvider
For configurable widgets. Uses async/await natively. Receives user intent configuration.
struct 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)
}
}
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.
struct InteractiveWidgetView: View {
let entry: FavoriteEntry
var body: some View {
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
Image(systemName: entry.isFavorite ? "star.fill" : "star")
}
}
}
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.
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
DeliveryLiveActivityView(context: context)
} dynamicIsland: { context in
DeliveryDynamicIsland(context: context)
}
}
}
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.
struct 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")
}
}
Lock Screen Widgets
Use accessory families and AccessoryWidgetBackground.
struct 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])
}
}
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. Apple budgets
40–70 refreshes per day
with entries at least 5 minutes apart. Use
Text(timerInterval:countsDown:)for live countdowns instead of burning timeline entries.
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.
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. -
Letting WidgetKit absorb sibling-skill work. Keep full Live Activity lifecycle in
activitykitand full App Intent modeling inapp-intents. -
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
.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.
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:)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
- 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
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

accessorysetupkit
Privacy-preserving Bluetooth and Wi-Fi accessory discovery and setup for iOS 18+

activitykit
Build real-time Lock Screen and Dynamic Island Live Activities for iOS with ActivityKit.

adattributionkit
Privacy-preserving ad attribution for iOS 17.4+ measuring conversions without exposing user data.

alarmkit
Schedule system alarms and countdown timers with Lock Screen, Dynamic Island, and Apple Watch integration for iOS 26+.

tech-news-digest
Generate tech news digests with unified source model, quality scoring, and multi-format output. Six-source data collection from RSS feeds, Twitter/X KOLs, GitHub releases, GitHub Trending, Reddit, and web search. Pipeline-based scripts with retry mechanisms and deduplication. Supports Discord, email, and markdown templates.

pdf-to-markdown
[Document Processing] Use when you need to convert PDF files to Markdown with support for native text PDFs and scanned documents (OCR).