Claude Agent Skill · by Dpearson2699

Widgetkit

Install Widgetkit skill for Claude Code from dpearson2699/swift-ios-skills.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices
Works 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 pack
Source file
SKILL.md497 lines
Expand
---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)