WidgetKit and ActivityKit
Build home screen widgets, Lock Screen widgets, Live Activities, Dynamic Island
presentations, Control Center controls, and StandBy surfaces for iOS 26+.
See references/widgetkit-advanced.md for timeline strategies, push-based
updates, Xcode setup, and advanced patterns.
Contents
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
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. Add a Live Activity
- Define an
ActivityAttributes struct with a nested ContentState.
- Add
NSSupportsLiveActivities = YES to the app's Info.plist.
- Create an
ActivityConfiguration in the widget bundle with Lock Screen content
and Dynamic Island closures.
- Start the activity with
Activity.request(attributes:content:pushType:).
- Update with
activity.update(_:) and end with activity.end(_:dismissalPolicy:).
3. Add a Control Center control
- Define an
AppIntent for the action.
- 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.
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()
QuickActionControl()
}
}
Configuration Types
Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration
(recommended) for configurable widgets paired with AppIntentTimelineProvider.
StaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in
MyWidgetView(entry: entry)
}
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