Extensions & Widgets API Reference

SkillDev tools

Use when implementing widgets, Live Activities, Control Center controls, or app extensions - comprehensive API reference for WidgetKit, ActivityKit, App Groups, and extension lifecycle for iOS 14+

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Extensions & Widgets API Reference skill

What this skill tells your AI

The instructions your AI receives, as published by comeonoliver/skillshub in skills/CharlesWiltgen/Axiom/axiom-extensions-widgets-ref/SKILL.md and read by ahel’s review.

Overview

This skill provides comprehensive API reference for Apple's widget and extension ecosystem:

  • Standard Widgets (iOS 14+) — Home Screen, Lock Screen, StandBy widgets
  • Interactive Widgets (iOS 17+) — Buttons and toggles with App Intents
  • Live Activities (iOS 16.1+) — Real-time updates on Lock Screen and Dynamic Island
  • Control Center Widgets (iOS 18+) — System-wide quick controls
  • Liquid Glass Widgets (iOS 26+) — Accented rendering, glass effects, container backgrounds
  • visionOS Widgets (visionOS 2+) — Mounting styles, textures, proximity awareness
  • App Extensions — Shared data, lifecycle, entitlements

Widgets are SwiftUI archived snapshots rendered on a timeline by the system. Extensions are sandboxed executables bundled with your app.

When to Use This Skill

Use this skill when:

  • Implementing any type of widget (Home Screen, Lock Screen, StandBy)
  • Creating Live Activities for ongoing events
  • Building Control Center controls
  • Sharing data between app and extensions
  • Understanding widget timelines and refresh policies
  • Integrating widgets with App Intents
  • Adopting Liquid Glass rendering in widgets
  • Supporting watchOS or visionOS widgets
  • Implementing visionOS mounting styles, textures, or proximity awareness

Do NOT use this skill for:

  • Pure App Intents questions (use app-intents-ref skill)
  • SwiftUI layout issues (use swiftui-layout skill)
  • Performance optimization (use swiftui-performance skill)
  • Debugging crashes (use xcode-debugging skill)

Related Skills

  • app-intents-ref — App Intents for interactive widgets and configuration
  • swift-concurrency — Async/await patterns for widget data loading
  • swiftui-performance — Optimizing widget rendering
  • swiftui-layout — Complex widget layouts
  • extensions-widgets — Discipline skill with anti-patterns and debugging

Key Terminology

  • Timeline — Series of entries defining when/what content to display; system shows entries at specified times
  • TimelineProvider — Protocol supplying timeline entries (placeholder, snapshot, timeline generation)
  • TimelineEntry — Struct with widget data + display date
  • Timeline Budget — Daily limit (40-70) for timeline reloads
  • Budget-Exempt — Reloads that don't count (user-initiated, app foregrounding, system-initiated)
  • Widget Family — Size/shape (systemSmall, systemMedium, accessoryCircular, etc.)
  • App Groups — Entitlement for shared data container between app and extensions
  • ActivityAttributes — Static data (set once) + dynamic ContentState (updated during lifecycle)
  • ContentState — Changing part of ActivityAttributes; must be under 4KB total
  • Dynamic Island — iPhone 14 Pro+ Live Activity display; compact, minimal, and expanded sizes
  • ControlWidget — iOS 18+ widgets for Control Center, Lock Screen, and Action Button
  • Supplemental Activity Families — Enables Live Activities on Apple Watch or CarPlay

Part 1: Standard Widgets (iOS 14+)

Widget Configuration Types

StaticConfiguration

For widgets that don't require user configuration.

@main
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This widget displays...")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

AppIntentConfiguration (iOS 17+)

For widgets with user configuration using App Intents.

struct MyConfigurableWidget: Widget {
    let kind: String = "MyConfigurableWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(
            kind: kind,
            intent: SelectProjectIntent.self,
            provider: Provider()
        ) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Project Status")
        .description("Shows your selected project")
    }
}

Migration from IntentConfiguration: iOS 16 and earlier used IntentConfiguration with SiriKit intents. Migrate to AppIntentConfiguration for iOS 17+.

ActivityConfiguration

For Live Activities (covered in Live Activities section).

Choosing the Right Configuration

No user configuration needed? Use StaticConfiguration. Simple static options? Use AppIntentConfiguration with WidgetConfigurationIntent. Dynamic options from app data? Use AppIntentConfiguration + EntityQuery.

Quick Reference:

  • StaticConfiguration — No customization (weather, battery status)
  • AppIntentConfiguration (simple) — Fixed options (timer presets, theme selection)
  • AppIntentConfiguration (EntityQuery) — Dynamic list from app data (project/contact/playlist picker)
  • ActivityConfiguration — Live ongoing events (delivery tracking, workout progress, sports scores)

Widget Families

System Families (Home Screen)

  • systemSmall (~170×170, iOS 14+) — Single piece of info, icon
  • systemMedium (~360×170, iOS 14+) — Multiple data points, chart
  • systemLarge (~360×380, iOS 14+) — Detailed view, list
  • systemExtraLarge (~720×380, iOS 15+ iPad only) — Rich layouts, multiple views

Accessory Families (Lock Screen, iOS 16+)

  • accessoryCircular (~48×48pt) — Circular complication, icon or gauge
  • accessoryRectangular (~160×72pt) — Above clock, text + icon
  • accessoryInline (single line) — Above date, text only

Example: Supporting Multiple Families

struct MyWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
            if #available(iOSApplicationExtension 16.0, *) {
                switch entry.family {
                case .systemSmall:
                    SmallWidgetView(entry: entry)
                case .systemMedium:
                    MediumWidgetView(entry: entry)
                case .accessoryCircular:
                    CircularWidgetView(entry: entry)
                case .accessoryRectangular:
                    RectangularWidgetView(entry: entry)
                default:
                    Text("Unsupported")
                }
            } else {
                LegacyWidgetView(entry: entry)
            }
        }
        .supportedFamilies([
            .systemSmall,
            .systemMedium,
            .accessoryCircular,
            .accessoryRectangular
        ])
    }
}

Timeline System

TimelineProvider Protocol

Provides entries that define when the system should render your widget.

struct Provider: TimelineProvider {
    // Placeholder while loading
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), emoji: "😀")
    }

    // Shown in widget gallery
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), emoji: "📷")
        completion(entry)
    }

    // Actual timeline
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        let currentDate = Date()

        // Create entry every hour for 5 hours
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, emoji: "⏰")
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

TimelineReloadPolicy

Controls when the system requests a new timeline:

  • .atEnd — Reload after last entry
  • .after(date) — Reload at specific date
  • .never — No automatic reload (manual only)

Manual Reload

import WidgetKit

// Reload all widgets of this kind
WidgetCenter.shared.reloadAllTimelines()

// Reload specific kind
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

Performance & Budget Quick Reference

Timeline Refresh Budget

  • Daily budget: 40-70 reloads/day (varies by system load and engagement)
  • Budget-exempt: User-initiated reload, app foregrounding, widget added, system reboot
  • Strategic (4x/hour) — ~48 reloads/day, low battery impact
  • Aggressive (12x/hour) — Budget exhausted by 6 PM, high impact
  • On-demand only — 5-10 reloads/day, minimal impact
  • Reload on significant data changes and time-based events. Avoid speculative or cosmetic reloads.
// ✅ GOOD: Strategic intervals (15-60 min)
let entries = (0..<8).map { offset in
    let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
    return SimpleEntry(date: date, data: data)
}

Memory Limits

  • ~30MB for standard widgets, ~50MB for Live Activities — system terminates if exceeded
  • Load only what you need (e.g., loadRecentItems(limit: 10), not entire database)

Network Requests

Never make network requests in widget views — they won't complete before rendering. Fetch data in getTimeline() instead.

Timeline Generation

Complete getTimeline() in under 5 seconds. Cache expensive computations in the main app, read pre-computed data from shared container, limit to 10-20 entries.

View Rendering

Precompute everything in TimelineEntry, keep views simple. No expensive operations in body.

Images

  • Use asset catalog images or SF Symbols (fast)
  • Small images from shared container are acceptable
  • AsyncImage does NOT work in widgets
  • Large images cause memory termination

Part 2: Interactive Widgets (iOS 17+)

Button and Toggle

Interactive widgets use SwiftUI Button and Toggle with App Intents.

Button with App Intent

Button(intent: IncrementIntent()) {
    Label("Increment", systemImage: "plus.circle")
}

The intent updates shared data via App Groups in its perform() method. See axiom-app-intents-ref for full AppIntent definition syntax.

Toggle with App Intent

Same pattern as Button — use a Toggle bound to state, invoke intent on change:

Toggle(isOn: $isEnabled) {
    Text("Feature")
}
.onChange(of: isEnabled) { newValue in
    Task { try? await ToggleFeatureIntent(enabled: newValue).perform() }
}

The intent follows the same AppIntent structure with a @Parameter(title: "Enabled") var enabled: Bool. See axiom-app-intents-ref for full AppIntent definition syntax.

invalidatableContent Modifier

Provides visual feedback during App Intent execution.

struct MyWidgetView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(entry.status)
                .invalidatableContent() // Dims during intent execution

            Button(intent: RefreshIntent()) {
                Image(systemName: "arrow.clockwise")
            }
        }
    }
}

Effect: Content with .invalidatableContent() becomes slightly transparent while the associated intent executes, providing user feedback.

Animation System

contentTransition for Numeric Text

Text("\(entry.value)")
    .contentTransition(.numericText(value: Double(entry.value)))

Effect: Numbers smoothly count up or down instead of instantly changing.

View Transitions

VStack {
    if entry.showDetail {
        DetailView()
            .transition(.scale.combined(with: .opacity))
    }
}
.animation(.spring(response: 0.3), value: entry.showDetail)

Part 3: Configurable Widgets (iOS 17+)

WidgetConfigurationIntent

Define configuration parameters for your widget.

import AppIntents

struct SelectProjectIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Select Project"
    static var description = IntentDescription("Choose which project to display")

    @Parameter(title: "Project")
    var project: ProjectEntity?

    // Provide default value
    static var parameterSummary: some ParameterSummary {
        Summary("Show \(\.$project)")
    }
}

Entity and EntityQuery

Provide dynamic options for configuration.

struct ProjectEntity: AppEntity {
    var id: String
    var name: String

    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }
}

struct ProjectQuery: EntityQuery {
    func entities(for identifiers: [String]) async throws -> [ProjectEntity] {
        // Return projects matching these IDs
        return await ProjectStore.shared.projects(withIDs: identifiers)
    }

    func suggestedEntities() async throws -> [ProjectEntity] {
        // Return all available projects
        return await ProjectStore.shared.allProjects()
    }
}

Using Configuration in Provider

struct Provider: AppIntentTimelineProvider {
    func timeline(for configuration: SelectProjectIntent, in context: Context) async -> Timeline<SimpleEntry> {
        let project = configuration.project // Use selected project
        let entries = await generateEntries(for: project)
        return Timeline(entries: entries, policy: .atEnd)
    }
}

Part 4: Live Activities (iOS 16.1+)

ActivityAttributes

Defines static and dynamic data for a Live Activity.

import ActivityKit

struct PizzaDeliveryAttributes: ActivityAttributes {
    // Static data - set when activity starts, never changes
    struct ContentState: Codable, Hashable {
        // Dynamic data - updated throughout activity lifecycle
        var status: DeliveryStatus
        var estimatedDeliveryTime: Date
        var driverName: String?
    }

    // Static attributes
    var orderNumber: String
    var pizzaType: String
}

Key constraint: ActivityAttributes total data size must be under 4KB to start successfully.

Starting Activities

Request Authorization

import ActivityKit

let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabled

Start an Activity

let attributes = PizzaDeliveryAttributes(
    orderNumber: "12345",
    pizzaType: "Pepperoni"
)

let initialState = PizzaDeliveryAttributes.ContentState(
    status: .preparing,
    estimatedDeliveryTime: Date().addingTimeInterval(30 * 60)
)

let activity = try Activity.request(
    attributes: attributes,
    content: ActivityContent(state: initialState, staleDate: nil),
    pushType: nil // or .token for push notifications
)

Error Handling

Common Activity Errors

Always check ActivityAuthorizationInfo().areActivitiesEnabled before requesting. Handle these errors from Activity.request():

  • ActivityAuthorizationError — User denied Live Activities permission
  • ActivityError.dataTooLarge — ActivityAttributes exceeds 4KB; reduce attribute size
  • ActivityError.tooManyActivities — System limit reached (typically 2-3 simultaneous)

Store activity.id after successful request for later updates.

Updating Activities

Update with New Content

// Find active activity by stored ID
guard let activity = Activity<PizzaDeliveryAttributes>.activities
    .first(where: { $0.id == storedActivityID }) else { return }

let updatedState = PizzaDeliveryAttributes.ContentState(
    status: .onTheWay,
    estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
    driverName: "John"
)

await activity.update(
    ActivityContent(
        state: updatedState,
        staleDate: Date().addingTimeInterval(60) // Mark stale after 1 min
    )
)

Alert Configuration

await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
    title: "Pizza is here!",
    body: "Your \(attributes.pizzaType) pizza has arrived",
    sound: .default
))

Monitoring Activity Lifecycle

Use activity.activityStateUpdates async sequence to observe state changes (.active, .ended, .dismissed, .stale). Clean up stored activity IDs on .ended or .dismissed. Cancel the monitoring task in deinit.

Ending Activities

Dismissal Policies

await activity.end(
    ActivityContent(state: finalState, staleDate: nil),
    dismissalPolicy: .default
)

Dismissal policy options:

  • .immediate — Removes instantly
  • .default — Stays on Lock Screen for ~4 hours
  • .after(date) — Removes at specific time (e.g., .after(Date().addingTimeInterval(3600)))

Push Notifications for Live Activities

Request Push Token

let activity = try Activity.request(
    attributes: attributes,
    content: initialContent,
    pushType: .token // Request push token
)

// Monitor for push token
for await pushToken in activity.pushTokenUpdates {
    let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
    // Send to your server
    await sendTokenToServer(tokenString, activityID: activity.id)
}

Frequent Push Updates (iOS 18.2+)

Standard limit is ~10-12 pushes/hour. For live events (sports, stocks), add the com.apple.developer.activity-push-notification-frequent-updates entitlement for significantly higher limits.


Part 5: Dynamic Island (iOS 16.1+)

Presentation Types

Live Activities appear in the Dynamic Island with three size classes:

Compact (Leading + Trailing)

Shown when another Live Activity is expanded or when multiple activities are active.

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image(systemName: "timer")
    }
    DynamicIslandExpandedRegion(.trailing) {
        Text("\(entry.timeRemaining)")
    }
    // ...
} compactLeading: {
    Image(systemName: "timer")
} compactTrailing: {
    Text("\(entry.timeRemaining)")
        .frame(width: 40)
}

Minimal

Shown when more than two Live Activities are active (circular avatar).

DynamicIsland {
    // ...
} minimal: {
    Image(systemName: "timer")
        .foregroundStyle(.tint)
}

Expanded

Shown when user long-presses the compact view.

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image(systemName: "timer")
            .font(.title)
    }

    DynamicIslandExpandedRegion(.trailing) {
        VStack(alignment: .trailing) {
            Text("\(entry.timeRemaining)")
                .font(.title2.monospacedDigit())
            Text("remaining")
                .font(.caption)
        }
    }

    DynamicIslandExpandedRegion(.center) {
        // Optional center content
    }

    DynamicIslandExpandedRegion(.bottom) {
        HStack {
            Button(intent: PauseIntent()) {
                Label("Pause", systemImage: "pause.fill")
            }
            Button(intent: StopIntent()) {
                Label("Stop", systemImage: "stop.fill")
            }
        }
    }
}

Design Principles (From WWDC 2023-10194)

Concentric Alignment

Content should nest concentrically inside the Dynamic Island's rounded shape with even margins. Use Circle() or RoundedRectangle(cornerRadius:) — never sharp Rectangle() which pokes into corners.

Biological Motion

Dynamic Island animations should feel organic and elastic. Use .spring(response: 0.6, dampingFraction: 0.7) or .interpolatingSpring(stiffness: 300, damping: 25) instead of linear animations.


Part 6: Control Center Widgets (iOS 18+)

ControlWidget Protocol

Controls appear in Control Center, Lock Screen, and Action Button (iPhone 15 Pro+).

StaticControlConfiguration

For simple controls without configuration.

import WidgetKit
import AppIntents

struct TorchControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: "TorchControl") {
            ControlWidgetButton(action: ToggleTorchIntent()) {
                Label("Flashlight", systemImage: "flashlight.on.fill")
            }
        }
        .displayName("Flashlight")
        .description("Toggle flashlight")
    }
}

AppIntentControlConfiguration

For configurable controls.

struct TimerControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        AppIntentControlConfiguration(
            kind: "TimerControl",
            intent: ConfigureTimerIntent.self
        ) { configuration in
            ControlWidgetButton(action: StartTimerIntent(duration: configuration.duration)) {
                Label("\(configuration.duration)m Timer", systemImage: "timer")
            }
        }
    }
}

ControlWidgetButton

For discrete actions (one-shot operations).

ControlWidgetButton(action: PlayMusicIntent()) {
    Label("Play", systemImage: "play.fill")
}
.tint(.purple)

ControlWidgetToggle

For boolean state.

struct AirplaneModeControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: "AirplaneModeControl") {
            ControlWidgetToggle(
                isOn: AirplaneModeIntent.isEnabled,
                action: AirplaneModeIntent()
            ) { isOn in
                Label(isOn ? "On" : "Off", systemImage: "airplane")
            }
        }
    }
}

Value Providers (Async State)

For controls needing async state, pass a ControlValueProvider to StaticControlConfiguration:

struct ThermostatProvider: ControlValueProvider {
    func currentValue() async throws -> ThermostatValue {
        let temp = try await HomeManager.shared.currentTemperature()
        return ThermostatValue(temperature: temp)
    }
    var previewValue: ThermostatValue { ThermostatValue(temperature: 72) }
}

The provider value is passed to your control's closure: { value in ControlWidgetButton(...) }.

Configurable Controls

Use AppIntentControlConfiguration with a WidgetConfigurationIntent (same pattern as configurable widgets). Add .promptsForUserConfiguration() to show configuration UI when the user adds the control.

Control Refinements

  • .controlWidgetActionHint("Toggles flashlight") — VoiceOver accessibility hint
  • .displayName("My Control") / .description("...") — Shown in Control Center UI

Part 7: iOS 18+ Updates

Accented Rendering and Liquid Glass

Widget rendering modes span multiple iOS versions: widgetAccentable() (iOS 16+), WidgetAccentedRenderingMode (iOS 18+), and Liquid Glass effects like glassEffect() and GlassEffectContainer (iOS 26+). Detect the mode and adapt layout accordingly.

Detecting Rendering Mode

struct MyWidgetView: View {
    @Environment(\.widgetRenderingMode) var renderingMode

    var body: some View {
        if renderingMode == .accented {
            // Simplified layout — opaque images tinted white, background replaced with glass
        } else {
            // Standard full-color layout
        }
    }
}

widgetAccentable(_:)

Marks views as part of the accent group. In accented mode, accent-group views are tinted separately from primary-group views, creating visual hierarchy.

HStack {
    VStack(alignment: .leading) {
        Text("Title")
            .font(.headline)
            .widgetAccentable()  // Accent group — tinted in accented mode
        Text("Subtitle")
            // Primary group by default
    }
    Image(systemName: "star.fill")
        .widgetAccentable()  // Also accent group
}

WidgetAccentedRenderingMode

Controls how images render in accented mode. Apply to Image views:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
63
Forks
22
Last commit
Jun 2026
Advanced
Catalog kind
skill
Gateway key
axiom-extensions-widgets-ref
Source
github.com/comeonoliver/skillshub