Install
Terminal · npx$
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practicesWorks with Paperclip
How Widgetkit fits into a Paperclip company.
Widgetkit drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
S
SaaS FactoryPaired
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
$27$59
Explore packSource file
SKILL.md497 linesExpandCollapse
---name: widgetkitdescription: "Implement, review, or improve widgets, Live Activities, and controls using WidgetKit and ActivityKit. Use when building home screen, Lock Screen, or StandBy widgets with timeline providers; when creating interactive widgets with Button/Toggle and AppIntent actions; when adding Live Activities with Dynamic Island layouts (compact, minimal, expanded); when building Control Center widgets with ControlWidgetButton/ControlWidgetToggle; when configuring widget families, refresh budgets, deep links, push-based reloads, or Liquid Glass rendering; or when setting up widget extensions, App Groups, and entitlements."--- # WidgetKit and ActivityKit Build home screen widgets, Lock Screen widgets, Live Activities, Dynamic Islandpresentations, Control Center controls, and StandBy surfaces for iOS 26+. See [references/widgetkit-advanced.md](references/widgetkit-advanced.md) for timeline strategies, push-basedupdates, Xcode setup, and advanced patterns. ## Contents - [Workflow](#workflow)- [Widget Protocol and WidgetBundle](#widget-protocol-and-widgetbundle)- [Configuration Types](#configuration-types)- [TimelineProvider](#timelineprovider)- [AppIntentTimelineProvider](#appintenttimelineprovider)- [Widget Families](#widget-families)- [Interactive Widgets (iOS 17+)](#interactive-widgets-ios-17)- [Live Activities and Dynamic Island](#live-activities-and-dynamic-island)- [Control Center Widgets (iOS 18+)](#control-center-widgets-ios-18)- [Lock Screen Widgets](#lock-screen-widgets)- [StandBy Mode](#standby-mode)- [iOS 26 Additions](#ios-26-additions)- [Common Mistakes](#common-mistakes)- [Review Checklist](#review-checklist)- [References](#references) ## Workflow ### 1. Create a new widget 1. Add a Widget Extension target in Xcode (File > New > Target > Widget Extension).2. Enable App Groups for shared data between the app and widget extension.3. Define a `TimelineEntry` struct with a `date` property and display data.4. Implement a `TimelineProvider` (static) or `AppIntentTimelineProvider` (configurable).5. Build the widget view using SwiftUI, adapting layout per `WidgetFamily`.6. Declare the `Widget` conforming struct with a configuration and supported families.7. Register all widgets in a `WidgetBundle` annotated with `@main`. ### 2. Add a Live Activity 1. Define an `ActivityAttributes` struct with a nested `ContentState`.2. Add `NSSupportsLiveActivities = YES` to the app's Info.plist.3. Create an `ActivityConfiguration` in the widget bundle with Lock Screen content and Dynamic Island closures.4. Start the activity with `Activity.request(attributes:content:pushType:)`.5. Update with `activity.update(_:)` and end with `activity.end(_:dismissalPolicy:)`. ### 3. Add a Control Center control 1. Define an `AppIntent` for the action.2. Create a `ControlWidgetButton` or `ControlWidgetToggle` in the widget bundle.3. 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`. ```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]) }}``` ### WidgetBundle Use `WidgetBundle` to expose multiple widgets from a single extension. ```swift@mainstruct MyAppWidgets: WidgetBundle { var body: some Widget { OrderStatusWidget() FavoritesWidget() DeliveryActivityWidget() // Live Activity QuickActionControl() // Control Center }}``` ## Configuration Types Use `StaticConfiguration` for non-configurable widgets. Use `AppIntentConfiguration`(recommended) for configurable widgets paired with `AppIntentTimelineProvider`. ```swift// StaticStaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in MyWidgetView(entry: entry)}// ConfigurableAppIntentConfiguration(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: ```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))) } }}``` ## AppIntentTimelineProvider 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) }}``` ## Widget Families ### System Families (Home Screen) | Family | Platform ||---|---|| `.systemSmall` | iOS, iPadOS, macOS, CarPlay (iOS 26+) || `.systemMedium` | iOS, iPadOS, macOS || `.systemLarge` | iOS, iPadOS, macOS || `.systemExtraLarge` | iPadOS only | ### Accessory Families (Lock Screen / watchOS) | Family | Platform ||---|---|| `.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) }}``` ## Interactive Widgets (iOS 17+) Use `Button` and `Toggle` with `AppIntent` conforming types to perform actionsdirectly from a widget without launching the app. ```swiftstruct ToggleFavoriteIntent: AppIntent { static var title: LocalizedStringResource = "Toggle Favorite" @Parameter(title: "Item ID") var itemID: String func perform() async throws -> some IntentResult { await DataStore.shared.toggleFavorite(itemID) return .result() }} struct InteractiveWidgetView: View { let entry: FavoriteEntry var body: some View { HStack { Text(entry.itemName) Spacer() Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) { Image(systemName: entry.isFavorite ? "star.fill" : "star") } } .padding() }}``` ## Live Activities and Dynamic Island ### ActivityAttributes Define the static and dynamic data model. ```swiftstruct DeliveryAttributes: ActivityAttributes { struct ContentState: Codable, Hashable { var driverName: String var estimatedDeliveryTime: ClosedRange<Date> var currentStep: DeliveryStep } var orderNumber: Int var restaurantName: String}``` ### ActivityConfiguration Provide Lock Screen content and Dynamic Island closures in the widget bundle. ```swiftstruct DeliveryActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: DeliveryAttributes.self) { context in VStack(alignment: .leading) { Text(context.attributes.restaurantName).font(.headline) HStack { Text("Driver: \(context.state.driverName)") Spacer() Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true) } } .padding() } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Image(systemName: "box.truck.fill").font(.title2) } DynamicIslandExpandedRegion(.trailing) { Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true) .font(.caption) } DynamicIslandExpandedRegion(.center) { Text(context.attributes.restaurantName).font(.headline) } DynamicIslandExpandedRegion(.bottom) { HStack { ForEach(DeliveryStep.allCases, id: \.self) { step in Image(systemName: step.icon) .foregroundStyle(step <= context.state.currentStep ? .primary : .tertiary) } } } } compactLeading: { Image(systemName: "box.truck.fill") } compactTrailing: { Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true) .frame(width: 40).monospacedDigit() } minimal: { Image(systemName: "box.truck.fill") } } }}``` ### Dynamic Island Regions | Region | Position ||---|---|| `.leading` | Left of the TrueDepth camera; wraps below || `.trailing` | Right of the TrueDepth camera; wraps below || `.center` | Directly below the camera || `.bottom` | Below all other regions | ### Starting, Updating, and Ending ```swift// Startlet attributes = DeliveryAttributes(orderNumber: 123, restaurantName: "Pizza Place")let state = DeliveryAttributes.ContentState( driverName: "Alex", estimatedDeliveryTime: Date()...Date().addingTimeInterval(1800), currentStep: .preparing)let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)let activity = try Activity.request(attributes: attributes, content: content, pushType: .token) // Update (optionally with alert)let updated = ActivityContent(state: newState, staleDate: nil, relevanceScore: 90)await activity.update(updated)await activity.update(updated, alertConfiguration: AlertConfiguration( title: "Order Update", body: "Your driver is nearby!", sound: .default)) // Endlet final = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)await activity.end(final, dismissalPolicy: .after(.now.addingTimeInterval(3600)))``` ## Control Center Widgets (iOS 18+) ```swift// Button controlstruct OpenCameraControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "OpenCamera") { ControlWidgetButton(action: OpenCameraIntent()) { Label("Camera", systemImage: "camera.fill") } } .displayName("Open Camera") }} // Toggle control with value providerstruct 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`. ```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]) }}``` ## StandBy Mode `.systemSmall` widgets automatically appear in StandBy (iPhone on charger inlandscape). Use `@Environment(\.widgetLocation)` for conditional rendering: ```swift@Environment(\.widgetLocation) var location// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.``` ## iOS 26 Additions ### Liquid Glass Support Adapt widgets to the Liquid Glass visual style using `WidgetAccentedRenderingMode`. | Mode | Description ||---|---|| `.accented` | Accented rendering for Liquid Glass || `.accentedDesaturated` | Accented with desaturation || `.desaturated` | Fully desaturated || `.fullColor` | Full-color rendering | ### WidgetPushHandler Enable push-based timeline reloads without scheduled polling. ```swiftstruct MyWidgetPushHandler: WidgetPushHandler { func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) { let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined() // Send tokenString to your server }}``` ### CarPlay Widgets `.systemSmall` widgets render in CarPlay on iOS 26+. Ensure small widget layoutsare legible at a glance for driver safety. ## Common Mistakes 1. **Using IntentTimelineProvider instead of AppIntentTimelineProvider.** `IntentTimelineProvider` is the older SiriKit Intents-based provider. Prefer `AppIntentTimelineProvider` with the App Intents framework for new widgets. 2. **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. 3. **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. 4. **Performing network calls in placeholder().** `placeholder(in:)` must return synchronously with sample data. Use `getTimeline` or `timeline(for:in:)` for async work. 5. **Missing NSSupportsLiveActivities Info.plist key.** Live Activities will not start without `NSSupportsLiveActivities = YES` in the host app's Info.plist. 6. **Using the deprecated contentState API.** Use `ActivityContent` for all `Activity.request`, `update`, and `end` calls. The `contentState`-based methods are deprecated. 7. **Not handling the stale state.** Check `context.isStale` in Live Activity views and show a fallback (e.g., "Updating...") when content is outdated. 8. **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. 9. **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. 10. **Not testing on device.** Dynamic Island and StandBy behavior 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 `AppIntent` with `Button`/`Toggle` only- [ ] Live Activity: `NSSupportsLiveActivities = YES`; `ActivityContent` used; Dynamic Island closures implemented- [ ] `activity.end(_:dismissalPolicy:)` called; controls use `StaticControlConfiguration`/`AppIntentControlConfiguration`- [ ] Timeline entries and Intent types are Sendable; tested on device ## References - Advanced guide: [references/widgetkit-advanced.md](references/widgetkit-advanced.md)- Apple docs: [WidgetKit](https://sosumi.ai/documentation/widgetkit) | [ActivityKit](https://sosumi.ai/documentation/activitykit) | [Keeping a widget up to date](https://sosumi.ai/documentation/widgetkit/keeping-a-widget-up-to-date)