axiom-extensions-widgets-ref by CharlesWiltgen
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+
Content & Writing
238 Stars
16 Forks
Updated Jan 16, 2026, 03:16 PM
Why Use This
This skill provides specialized capabilities for CharlesWiltgen's codebase.
Use Cases
- Developing new features in the CharlesWiltgen repository
- Refactoring existing code to follow CharlesWiltgen standards
- Understanding and working with CharlesWiltgen's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 1287 Lines
Total Files 1
Total Size 0 B
License MIT
---
name: axiom-extensions-widgets-ref
description: 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+
license: MIT
compatibility: iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, visionOS 2+
metadata:
version: "1.0.0"
---
# 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 DatabasePool(path: dbPath, configuration: readOnlyConfig)`
- Set `configuration.readonly = true` in widget to prevent accidental writes
---
## Resources
**WWDC**: 2025-278, 2024-10157, 2024-10068, 2024-10098, 2023-10028, 2023-10194, 2022-10184, 2022-10185
**Docs**: /widgetkit, /activitykit, /appintents
**Skills**: axiom-app-intents-ref, axiom-swift-concurrency, axiom-swiftui-performance, axiom-swiftui-layout, axiom-extensions-widgets
---
**Version**: 0.9 | **Platforms**: iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, visionOS 2+
Name Size