# Axiom Extensions Widgets Ref

> 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+

- Skill: `charleswiltgen/axiom-extensions-widgets-ref` (Agent Skill)
- Install (CLI): `npx skillmds@latest add charleswiltgen/axiom-extensions-widgets-ref`
- Raw SKILL.md: https://api.skillmd.com/api/skills/charleswiltgen/axiom-extensions-widgets-ref/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: charleswiltgen (https://skillmd.com/u/charleswiltgen)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/charleswiltgen/axiom-extensions-widgets-ref

---


# Extensions & Widgets API Reference

## 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.

```swift
@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.

```swift
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

```swift
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.

```swift
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

```swift
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.

```swift
// ✅ 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

```swift
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:

```swift
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.

```swift
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

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

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

### View Transitions

```swift
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.

```swift
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.

```swift
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

```swift
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.

```swift
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

```swift
import ActivityKit

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

### Start an Activity

```swift
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

```swift
// 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

```swift
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

```swift
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

```swift
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.

```swift
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).

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

### Expanded

Shown when user long-presses the compact view.

```swift
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.

```swift
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.

```swift
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).

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

## ControlWidgetToggle

For boolean state.

```swift
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`:

```swift
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

```swift
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.

```swift
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:

```swift
Image("myPhoto")
    .widgetAccentedRenderingMode(.accented)      // Tinted with accent color
Image("myIcon")
    .widgetAccentedRenderingMode(.monochrome)     // Rendered as monochrome
Image("myBadge")
    .widgetAccentedRenderingMode(.fullColor)       // Keeps original colors (opt-out)
```

**Best practices**: Display full-color images only in `.fullColor` rendering mode. Use `.widgetAccentable()` strategically for visual hierarchy. Test with multiple accent colors and background images.

### Container Backgrounds

```swift
VStack { /* content */ }
    .containerBackground(for: .widget) {
        Color.blue.opacity(0.2)
    }
```

In accented mode, the system removes the background and replaces it with themed glass. To prevent removal (excludes widget from iPad Lock Screen, StandBy):

```swift
.containerBackgroundRemovable(false)
```

### Liquid Glass in Custom Widget Elements

```swift
Text("Label")
    .padding()
    .glassEffect()  // Default capsule shape

Image(systemName: "star.fill")
    .frame(width: 60, height: 60)
    .glassEffect(.regular, in: .rect(cornerRadius: 12))

Button("Action") { }
    .buttonStyle(.glass)
```

Combine multiple glass elements with `GlassEffectContainer`:

```swift
GlassEffectContainer(spacing: 20.0) {
    HStack(spacing: 20.0) {
        Image(systemName: "cloud")
            .frame(width: 60, height: 60)
            .glassEffect()
        Image(systemName: "sun")
            .frame(width: 60, height: 60)
            .glassEffect()
    }
}
```

## Cross-Platform Support

### visionOS Widgets (visionOS 2+)

visionOS widgets are 3D objects placed in physical space — mounted on surfaces or floating. They support unique spatial features.

#### Mounting Styles

Widgets can be elevated (on top of surfaces) or recessed (embedded into vertical surfaces like walls):

```swift
.supportedMountingStyles([.elevated, .recessed])  // Default is both
// .supportedMountingStyles([.recessed])           // Wall-only widget
```

If limited to `.recessed`, users cannot place the widget on horizontal surfaces.

#### Widget Textures

Two visual textures for spatial appearance:

```swift
.widgetTexture(.glass)   // Default — transparent glass-like appearance
.widgetTexture(.paper)   // Poster-like look, effective with extra-large sizes
```

#### Proximity Awareness (levelOfDetail)

Widgets adapt to user distance automatically. The system animates transitions between detail levels:

```swift
@Environment(\.levelOfDetail) var levelOfDetail

var body: some View {
    VStack {
        Text(entry.value)
            .font(levelOfDetail == .simplified ? .largeTitle : .title)
    }
}
```

Values: `.default` (close viewing) and `.simplified` (distance viewing — use larger text, fewer details).

#### visionOS Widget Families

visionOS supports all system families plus extra-large sizes:

```swift
.supportedFamilies([
    .systemSmall, .systemMedium, .systemLarge,
    .systemExtraLarge,
    .systemExtraLargePortrait  // visionOS-specific portrait orientation
])
```

Extra-large families are particularly effective with `.widgetTexture(.paper)` for poster-like displays.

#### Background Detection

Detect whether the widget background is visible (removed in accented mode):

```swift
@Environment(\.showsWidgetContainerBackground) var showsBackground
```

### CarPlay (iOS 18+)
Add `.supplementalActivityFamilies([.medium])` to `ActivityConfiguration`. Uses StandBy-style full-width dashboard presentation.

### macOS Menu Bar
Live Activities from paired iPhone appear automatically in macOS Sequoia+ menu bar. No code changes required.

### watchOS Controls (11+)
`ControlWidget` works identically on watchOS — available in Control Center, Action Button, and Smart Stack. Same `StaticControlConfiguration` / `ControlWidgetButton` pattern as iOS.

## Relevance Widgets (iOS 18+)

Use `.relevanceConfiguration(for:score:attributes:)` to help the system promote widgets in Smart Stack. Attributes include `.location(CLLocation)`, `.timeOfDay(DateInterval)`, and `.activity(String)` for context-aware ranking.

## Push Notification Updates (iOS 18+)

Implement `PKPushRegistryDelegate` and handle `.widgetKit` push type to receive server-to-widget pushes. Update shared container data and call `WidgetCenter.shared.reloadAllTimelines()`. Pushes to iPhone automatically sync to Apple Watch and CarPlay.

---

# Part 8: App Groups & Data Sharing

## App Groups Entitlement

Required for sharing data between your app and extensions.

### Configuration

1. Xcode: Targets → Signing & Capabilities → Add "App Groups"
2. Identifier format: `group.com.company.appname`
3. Enable for BOTH main app target AND extension target

## Shared Containers

### Access Shared Container

```swift
let sharedContainer = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!

let dataFileURL = sharedContainer.appendingPathComponent("widgetData.json")
```

### UserDefaults with App Groups

```swift
// Main app - write data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
shared.set("Updated value", forKey: "myKey")

// Widget extension - read data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
let value = shared.string(forKey: "myKey")
```

### Core Data with App Groups

Point `NSPersistentStoreDescription` at the shared container URL:

```swift
let sharedStoreURL = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!.appendingPathComponent("MyApp.sqlite")

let description = NSPersistentStoreDescription(url: sharedStoreURL)
container.persistentStoreDescriptions = [description]
```

## IPC Communication

- **Background URL Session** — Set `config.sharedContainerIdentifier` to your App Group ID for downloads accessible by extensions
- **Darwin Notification Center** — Use `CFNotificationCenterPostNotification` / `CFNotificationCenterAddObserver` with `CFNotificationCenterGetDarwinNotifyCenter()` for simple cross-process signals (e.g., notify widget to call `WidgetCenter.shared.reloadAllTimelines()`)

---

# Part 9: watchOS Integration

## supplementalActivityFamilies (watchOS 11+)

Add `.supplementalActivityFamilies([.small])` to `ActivityConfiguration` to show Live Activities on Apple Watch Smart Stack (same modifier used for CarPlay with `.medium`).

## activityFamily Environment

Use `@Environment(\.activityFamily)` to adapt layout — check for `.small` (watchOS) vs iPhone layout.

## Always On Display

Use `@Environment(\.isLuminanceReduced)` to simplify views for Always On Display — reduce detail, use white text, larger fonts. Combine with `@Environment(\.colorScheme)` for proper dark mode handling.

## Update Budgeting (watchOS)

watchOS updates sync automatically with iPhone via push notifications. Updates may be delayed if watch is out of Bluetooth range.

---

# Part 10: Practical Workflows

## Building Your First Widget

For a complete step-by-step tutorial with working code examples, see Apple's [Building Widgets Using WidgetKit and SwiftUI](https://developer.apple.com/documentation/widgetkit/building-widgets-using-widgetkit-and-swiftui) sample project.

**Key steps**: Add widget extension target, configure App Groups, implement TimelineProvider, design SwiftUI view, update from main app. See Expert Review Checklist below for production requirements.

---

## Expert Review Checklist

### Before Shipping Widgets

**Architecture**:
- [ ] App Groups entitlement configured in app AND extension
- [ ] Group identifier matches exactly in both targets
- [ ] Shared container used for ALL data sharing
- [ ] No `UserDefaults.standard` in widget code

**Performance**:
- [ ] Timeline generation completes in < 5 seconds
- [ ] No network requests in widget views
- [ ] Timeline has reasonable refresh intervals (≥ 15 min)
- [ ] Entry count reasonable (< 20-30 entries)
- [ ] Memory usage under limits (~30MB widgets, ~50MB activities)
- [ ] Images optimized (asset catalog or SF Symbols preferred)

**Data & State**:
- [ ] Widget handles missing/nil data gracefully
- [ ] Entry dates in chronological order
- [ ] Placeholder view looks reasonable
- [ ] Snapshot view representative of actual use

**User Experience**:
- [ ] Widget appears in widget gallery
- [ ] configurationDisplayName clear and concise
- [ ] description explains widget purpose
- [ ] All supported families tested and look correct
- [ ] Text readable on both light and dark backgrounds
- [ ] Interactive elements (buttons/toggles) work correctly

**Live Activities** (if applicable):
- [ ] ActivityAttributes under 4KB
- [ ] Authorization checked before starting
- [ ] Activity ends when event completes
- [ ] Proper dismissal policy set
- [ ] watchOS support configured if relevant (supplementalActivityFamilies)
- [ ] Dynamic Island layouts tested (compact, minimal, expanded)

**Liquid Glass** (if applicable):
- [ ] `widgetAccentable()` applied for visual hierarchy in accented mode
- [ ] `WidgetAccentedRenderingMode` set on images (`.accented`, `.monochrome`, or `.fullColor`)
- [ ] Tested with multiple accent colors and background images
- [ ] Container background configured with `.containerBackground(for: .widget)`

**visionOS** (if applicable):
- [ ] Mounting styles configured (`.elevated`, `.recessed`, or both)
- [ ] Widget texture chosen (`.glass` or `.paper`)
- [ ] `levelOfDetail` handled for proximity-aware layouts
- [ ] Extra-large families supported if appropriate (`.systemExtraLarge`, `.systemExtraLargePortrait`)
- [ ] Tested at different distances for proximity transitions

**Control Center Widgets** (if applicable):
- [ ] ControlValueProvider async and fast (< 1 second)
- [ ] previewValue provides reasonable fallback
- [ ] displayName and description set
- [ ] Tested in Control Center, Lock Screen, Action Button

**Testing**:
- [ ] Tested on actual device (not just simulator)
- [ ] Tested adding/removing widget
- [ ] Tested app data changes → widget updates
- [ ] Tested force-quit app → widget still works
- [ ] Tested low memory scenarios
- [ ] Tested all iOS versions you support
- [ ] Tested with no internet connection

---

## Testing Guidance

### Unit Testing Pattern

Test `placeholder()`, `getSnapshot()`, and `getTimeline()` methods. Save test data to shared container, call `getTimeline()` with a mock context, assert entries are non-empty and contain expected data. Use `waitForExpectations(timeout: 5.0)` for async timeline generation.

### Manual Testing Checklist
- Add widget to Home Screen, verify widget gallery, all supported sizes, data matches app
- Change data in main app, observe widget updates, force-quit app, reboot device
- Delete all app data (graceful handling), disable network (offline), Low Power Mode, multiple instances
- Monitor memory in Xcode Debug Navigator, check timeline generation time in Console, test on older devices

### Debugging Tips
- Add `print()` logging in `getTimeline()` to verify it's being called and data is loaded
- Verify App Groups: print `FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)` in both app and widget — paths must match
- After data changes in main app, call `WidgetCenter.shared.reloadAllTimelines()`

---

# Part 11: Troubleshooting

**Widget not appearing in gallery**: Check `WidgetBundle` includes it, verify `supportedFamilies()`, check extension's "Skip Install" = NO, verify deployment target matches app.

## Widget Not Refreshing

**Symptoms**: Widget shows stale data, doesn't update

**Diagnostic Steps**:
1. Check timeline policy (`.atEnd` vs `.after()` vs `.never`)
2. Verify you're not exceeding daily budget (40-70 reloads)
3. Check if `getTimeline()` is being called (add logging)
4. Ensure App Groups configured correctly for shared data

**Solution**:
```swift
// Manual reload from main app when data changes
import WidgetKit

WidgetCenter.shared.reloadAllTimelines()
// or
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
```

## Data Not Shared Between App and Widget

**Symptoms**: Widget shows default/empty data

**Diagnostic Steps**:
1. Verify App Groups entitlement in BOTH targets
2. Check group identifier matches exactly
3. Ensure using same suiteName in both targets
4. Check file path if using shared container

**Solution**:
```swift
// Both app AND extension must use:
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!

// NOT:
let shared = UserDefaults.standard  // ❌ Different containers
```

## Live Activity Won't Start

**Symptoms**: `Activity.request()` throws error

**Common Errors**:

**"Activity size exceeds 4KB"**:
```swift
// ❌ BAD: Large images in attributes
struct MyAttributes: ActivityAttributes {
    var productImage: UIImage  // Too large!
}

// ✅ GOOD: Use asset catalog names
struct MyAttributes: ActivityAttributes {
    var productImageName: String  // Reference to asset
}
```

**"Activities not enabled"**:
```swift
// Check authorization first
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
    throw ActivityError.notEnabled
}
```

## Interactive Widget Button Not Working

**Symptoms**: Tapping button does nothing

**Diagnostic Steps**:
1. Verify App Intent's `perform()` returns `IntentResult`
2. Check intent is imported in widget target
3. Ensure button uses `intent:` parameter, not `action:`
4. Check Console for intent execution errors

**Solution**:
```swift
// ✅ CORRECT: Use intent parameter
Button(intent: MyIntent()) {
    Label("Action", systemImage: "star")
}

// ❌ WRONG: Don't use action closure
Button(action: { /* This won't work in widgets */ }) {
    Label("Action", systemImage: "star")
}
```

**Control Center widget slow**: Use async in `ControlValueProvider.currentValue()`, never block with `Thread.sleep`. Provide fast `previewValue` fallback.

**Widget shows wrong size**: Switch on `@Environment(\.widgetFamily)` in view, adapt layout per family, avoid hardcoded sizes.

**Timeline entries out of order**: Ensure entry dates are chronological. Use incrementing offsets from `Date()`.

**watchOS Live Activity not showing**: Add `.supplementalActivityFamilies([.small])` to `ActivityConfiguration`, verify watchOS 11+, check Bluetooth/pairing.

## Performance Issues

**Symptoms**: Widget rendering slow, battery drain

**Common Causes**:
- Too many timeline entries (> 100)
- Network requests in view code
- Heavy computation in `getTimeline()`
- Refresh intervals too frequent (< 15 min)

**Solution**:
```swift
// ✅ GOOD: Strategic intervals
let entries = (0..<8).map { offset in
    let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
    return SimpleEntry(date: date, data: precomputedData)
}

// ❌ BAD: Too frequent, too many entries
let entries = (0..<100).map { offset in
    let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
    return SimpleEntry(date: date, data: fetchFromNetwork())  // Network in timeline
}
```

---

## Debugging Widgets

### Simulator vs Device

- **Simulator**: Widgets refresh immediately; no budget limits apply. Useful for layout testing but misleading for refresh behavior.
- **Device**: Budget-limited (40-70 reloads/day). Test on device before shipping to verify real-world refresh timing.
- **Xcode Previews**: Work for layout but skip `getTimeline()`. Test timeline logic with unit tests or device runs.

### Common Debugging Workflow

1. Add `print()` in `getTimeline()` — verify it's called and data loads
2. Check Console.app filtered by widget extension process name
3. Use `WidgetCenter.shared.getCurrentConfigurations()` to verify registration
4. If widget shows old data after app update, verify App Groups container paths match

### Data Sharing Patterns

**SwiftData in Widgets** (iOS 17+):
- Create `ModelContainer` in widget with same schema as main app
- Use shared App Groups container: `ModelConfiguration(url: containerURL)`
- Widget reads only — never write from widget to avoid conflicts
- Main app calls `WidgetCenter.shared.reloadAllTimelines()` after writes

**GRDB/SQLite in Widgets**:
- Share database file via App Groups container
- Use `DatabasePool` (not `DatabaseQueue`) for concurrent reads
- Widget opens read-only connection: `try 

…(truncated)
