---
name: widgetkit
description: "WidgetKit surfaces on iOS 26+: timeline-driven widgets for the Home Screen, the Lock Screen and StandBy, interactive widgets using Button or Toggle with AppIntent actions, widget views for a Live Activity (Dynamic Island compact, expanded, minimal), Control Center controls with ControlWidgetButton or ControlWidgetToggle, families, deep links, the refresh budget, push reloads, accented rendering, extension setup, App Groups. Use when implementing or reviewing widgets, Live Activity views or controls. Not for Live Activity lifecycle or pushes (live-activities)."
metadata:
  source: multi-agent-pipeline
---

# WidgetKit

Widgets are small SwiftUI snapshots that the system renders from data your
extension hands it ahead of time. This skill targets iOS 26+ and covers every
surface WidgetKit and ActivityKit draw on: the Home Screen, the Lock Screen,
Live Activities and the Dynamic Island, Control Center and StandBy.

Related skills: `app-intents` for intent and entity design beyond what a widget
needs, `live-activities` for the Live Activity lifecycle (request, update, end),
push tokens and ActivityKit push contracts. This skill covers the widget views a
Live Activity renders.

Longer examples are in [the advanced reference](references/widgetkit-advanced.md).

## Workflows

### New widget

1. In Xcode choose File > New > Target > Widget Extension.
2. Turn on App Groups for both the app and the extension so they can share data.
3. Write a `TimelineEntry` type: a `date` plus whatever the view shows.
4. Write a provider: `TimelineProvider` for a fixed widget,
   `AppIntentTimelineProvider` for a configurable one.
5. Write the SwiftUI view and let it adapt to the current `WidgetFamily`.
6. Declare a `Widget` type with its configuration and supported families.
7. List every widget in one `WidgetBundle` and put `@main` on the bundle.

### Live Activity

1. Declare `ActivityAttributes` with a nested `ContentState`.
2. Set `NSSupportsLiveActivities` to `YES` in the main app's Info.plist.
3. Add an `ActivityConfiguration` to the widget bundle with the Lock Screen view
   and the Dynamic Island closures.
4. `Activity.request(attributes:content:pushType:)` starts it,
   `activity.update(_:)` changes it and `activity.end(_:dismissalPolicy:)` closes it.

### Control Center control

1. Write the `AppIntent` the control runs.
2. Build a `ControlWidgetButton` or `ControlWidgetToggle`.
3. Wrap it in `StaticControlConfiguration` or `AppIntentControlConfiguration`.

### Review

Go through the [checklist](#review-checklist) below.

## Widget and WidgetBundle

A widget is a type conforming to `Widget`. Its `body` returns a
`WidgetConfiguration`, and it carries a `kind` string that identifies it for
reloads.

```swift
struct TideWidget: Widget {
    let kind = "TideWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: TideProvider()) { entry in
            TideView(entry: entry)
        }
        .configurationDisplayName("Tides")
        .description("Next high and low water at your beach.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

@main
struct HarborWidgets: WidgetBundle {
    var body: some Widget {
        TideWidget()
        HarborActivityWidget()
        LighthouseControl()
    }
}
```

One `WidgetBundle` can expose ordinary widgets, Live Activity widgets and
Control Center controls from the same extension.

## Configuration Types

| Type | Use |
|---|---|
| `StaticConfiguration` | Widget with no user settings |
| `AppIntentConfiguration(kind:intent:provider:)` | Preferred for configurable widgets; pairs with `AppIntentTimelineProvider` |

Modifiers shared by both:

- `.configurationDisplayName(_:)`: title shown in the widget gallery.
- `.description(_:)`: gallery blurb.
- `.supportedFamilies(_:)`: the `WidgetFamily` cases you support.
- `.supplementalActivityFamilies(_:)`: extra Live Activity sizes, `.small` and `.medium`.

## TimelineProvider

For fixed widgets. It uses completion handlers and needs three methods.

```swift
struct TideProvider: TimelineProvider {
    func placeholder(in context: Context) -> TideEntry {
        .sample
    }

    func getSnapshot(in context: Context, completion: @escaping @Sendable (TideEntry) -> Void) {
        if context.isPreview {
            completion(.sample)
        } else {
            completion(TideStore.shared.latestEntry())
        }
    }

    func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<TideEntry>) -> Void) {
        Task {
            let reading = await TideStore.shared.loadReading()
            let current = TideEntry(date: .now, reading: reading)
            let refresh = Calendar.current.date(byAdding: .minute, value: 60, to: .now) ?? .now
            completion(Timeline(entries: [current], policy: .after(refresh)))
        }
    }
}
```

`placeholder(in:)` hands back sample data instantly. The snapshot shows sample
data when `context.isPreview` is true and real data otherwise. The protocol
declares both completion handlers `@Sendable`; keep that in the signature, or
Swift 6 rejects handing `completion` to the `Task` as a data race.

## AppIntentTimelineProvider

For configurable widgets. It is async/await from the start and receives the
user's intent.

```swift
struct BeachProvider: AppIntentTimelineProvider {
    typealias Entry = TideEntry
    typealias Intent = PickBeachIntent

    func placeholder(in context: Context) -> TideEntry { .sample }

    func snapshot(for configuration: PickBeachIntent, in context: Context) async -> TideEntry {
        context.isPreview ? .sample : await TideStore.shared.entry(for: configuration.beach)
    }

    func timeline(for configuration: PickBeachIntent, in context: Context) async -> Timeline<TideEntry> {
        let latest = await TideStore.shared.entry(for: configuration.beach)
        return Timeline(entries: [latest], policy: .atEnd)
    }
}
```

## Widget Families

| Family | Where it runs |
|---|---|
| `.systemSmall` | iOS, iPadOS, macOS, and CarPlay from iOS 26 |
| `.systemMedium`, `.systemLarge` | iOS, iPadOS, macOS |
| `.systemExtraLarge` | iPadOS only |
| `.accessoryCircular`, `.accessoryRectangular`, `.accessoryInline` | iOS, watchOS |
| `.accessoryCorner` | watchOS only |

```swift
struct TideView: View {
    @Environment(\.widgetFamily) private var family
    let entry: TideEntry

    var body: some View {
        switch family {
        case .accessoryInline: Text(entry.reading.shortLine)
        case .systemMedium: TideChartRow(entry: entry)
        default: TideSummary(entry: entry)
        }
    }
}
```

## Interactive Widgets (iOS 17+)

`Button` and `Toggle` that take an `AppIntent` run the action inside the widget
without opening the app.

```swift
struct WaterPlantIntent: AppIntent {
    static let title: LocalizedStringResource = "Water Plant"

    @Parameter(title: "Plant")
    var plantID: String

    init() {}
    init(plantID: String) { self.plantID = plantID }

    func perform() async throws -> some IntentResult {
        await PlantLog.shared.recordWatering(plantID)
        return .result()
    }
}

Button(intent: WaterPlantIntent(plantID: plant.id)) {
    Image(systemName: "drop.fill")
}
```

## Live Activities and Dynamic Island

Fields that never change go on the `ActivityAttributes` type; changing fields go
on its nested `ContentState`, which must be `Codable` and `Hashable`.

```swift
struct RepairAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var window: ClosedRange<Date>
        var stage: RepairStage
    }
    var ticketNumber: String
}
```

`ActivityConfiguration(for:)` takes the Lock Screen view in its main closure
and a `dynamicIsland` closure that declares the expanded regions plus
`compactLeading`, `compactTrailing` and `minimal`.

Expanded regions:

| Region | Placement |
|---|---|
| `.leading` | Beside the TrueDepth camera on its left, continuing below |
| `.trailing` | Beside the camera on its right, continuing below |
| `.center` | Right under the camera |
| `.bottom` | Under every other region |

Lifecycle:

```swift
let initial = RepairAttributes.ContentState(window: start...finish, stage: .diagnosing)
let opening = ActivityContent(state: initial, staleDate: finish, relevanceScore: 75)
let activity = try Activity.request(
    attributes: RepairAttributes(ticketNumber: "R-2291"),
    content: opening,
    pushType: .token
)

let next = ActivityContent(state: updatedState, staleDate: finish, relevanceScore: 90)
await activity.update(next)

let done = ActivityContent(state: closedState, staleDate: nil, relevanceScore: 0)
await activity.end(done, dismissalPolicy: .after(.now.addingTimeInterval(1800)))
```

A complete configuration with every region is in
[Live Activity Full ActivityConfiguration Example](references/widgetkit-advanced.md#live-activity-full-activityconfiguration-example).

## Control Center Widgets (iOS 18+)

Put a `ControlWidgetButton` or `ControlWidgetToggle` inside
`StaticControlConfiguration` or `AppIntentControlConfiguration`. Every control
performs an `AppIntent`.

```swift
struct LighthouseControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: "LighthouseControl") {
            ControlWidgetButton(action: FlashBeaconIntent()) {
                Label("Beacon", systemImage: "light.beacon.max")
            }
        }
        .displayName("Flash Beacon")
    }
}
```

A toggle with live state is in
[Control Center Control Examples](references/widgetkit-advanced.md#control-center-control-examples).

## Lock Screen Widgets

Use the accessory families and put `AccessoryWidgetBackground()` behind the
content for the system's translucent backing. See
[Lock Screen Accessory Widget Example](references/widgetkit-advanced.md#lock-screen-accessory-widget-example)
and [AccessoryWidgetBackground](references/widgetkit-advanced.md#accessorywidgetbackground).

## StandBy Mode

`.systemSmall` widgets show up in StandBy automatically when an iPhone charges
on its side. Read `@Environment(\.widgetLocation)` to tell where you are drawn:
`.homeScreen`, `.lockScreen`, `.standBy` or `.carPlay`.

## Design Patterns

- Reach for `Gauge` before drawing arcs by hand. `.gaugeStyle(.accessoryCircular)`
  suits circular Lock Screen widgets and `.linearCapacity` suits Home Screen
  capacity bars; the system then handles styling, accessibility and rendering modes.
- From iOS 17, give the widget its background through
  `.containerBackground(_:for: .widget)` instead of padding plus `.background`.
- Draw dense visuals such as sparklines or tiny bar charts with `Canvas`. Losing
  per-element accessibility is acceptable because the whole widget is one tap target.
- Budget: roughly 40 to 70 refreshes a day, with entries 5 minutes apart or more.
  For countdowns use `Text(timerInterval:countsDown:)` instead of one entry per tick.

## iOS 18 Additions

- `WidgetAccentedRenderingMode` (`.accented`, `.accentedDesaturated`,
  `.desaturated`, `.fullColor`), applied with `.widgetAccentedRenderingMode(_:)`,
  controls how images adapt to accented (tinted) rendering. Available from
  iOS 18, macOS 15 and watchOS 11; on visionOS from 26.
- `ControlPushHandler.pushTokensDidChange(controls:)` gives a push token per
  control; an APNs push to it reloads the control.

## iOS 26 Additions

- `WidgetPushHandler.pushTokenDidChange(_:widgets:)` gives you a token to send to
  your server; an APNs push to it reloads the widget. See
  [Reloading timelines by push](references/widgetkit-advanced.md#reloading-timelines-by-push).
- `.systemSmall` widgets render in CarPlay from iOS 26. Keep them readable at a
  glance so a driver is never pulled into detail.

## Common Mistakes

- **Old provider.** `IntentTimelineProvider` belongs to the SiriKit Intents era;
  write `AppIntentTimelineProvider`.
- **Burning the budget.** Calling `WidgetCenter.shared.reloadTimelines(ofKind:)`
  after every small change drains refreshes. Batch changes and pick the right
  `TimelineReloadPolicy`.
- **No App Group.** The extension runs in its own process. Share data through
  `UserDefaults(suiteName:)` or the shared container.
- **Networking in `placeholder()`.** It must return sample data synchronously;
  fetch in `getTimeline` or `timeline(for:in:)`.
- **Missing plist key.** If the main app's Info.plist lacks
  `NSSupportsLiveActivities = YES`, no Live Activity will start.
- **Deprecated state APIs.** The request, update and end calls that take a bare
  `contentState` are deprecated; pass `ActivityContent`.
- **Ignoring staleness.** Read `context.isStale` in the Live Activity view and
  show something like "Updating..." when it is true.
- **Work in the view.** Widget views render in a memory-capped process. Compute
  in the provider and pass ready-to-show values.
- **Assuming full color on the Lock Screen.** Lock Screen widgets draw in
  `.vibrant` or `.accented`, not `.fullColor`. Check
  `@Environment(\.widgetRenderingMode)` and never carry meaning by color alone.
- **Trusting the Simulator.** Dynamic Island and StandBy behave differently on
  real devices; test there.

## Review Checklist

- [ ] App and extension share the same App Groups entitlement
- [ ] `@main` sits on the `WidgetBundle`, not on a single widget
- [ ] `placeholder(in:)` is synchronous; the snapshot is quick when `isPreview`
- [ ] Reload policy fits how often data changes; `reloadTimelines(ofKind:)` fires only on real changes
- [ ] Layout adapts per family; accessory widgets checked in `.vibrant`
- [ ] Interactive elements are `Button` or `Toggle` driven by an `AppIntent`
- [ ] Live Activity: Info.plist key present, `ActivityContent` used, all Dynamic Island closures written, `activity.end(_:dismissalPolicy:)` reached
- [ ] Controls use `StaticControlConfiguration` or `AppIntentControlConfiguration`
- [ ] Entry and intent types conform to `Sendable`
- [ ] Verified on a physical device

## References

- Longer patterns: [advanced reference](references/widgetkit-advanced.md)
- [WidgetKit](https://developer.apple.com/documentation/widgetkit)
- [ActivityKit](https://developer.apple.com/documentation/activitykit)
- [Keeping a widget up to date](https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date)
