widgets-live-activities
SKILL.md
WidgetKit and Live Activities
Comprehensive guide to WidgetKit for Home Screen widgets, Live Activities, Dynamic Island, and Control Center integration in iOS 26.
Prerequisites
- iOS 17+ for interactive widgets (iOS 26 recommended)
- Xcode 26+
- Widget Extension target
Widget Extension Setup
Creating Widget Target
- File → New → Target
- Select "Widget Extension"
- Name your widget
- Enable "Include Live Activity" if needed
- Enable "Include Configuration App Intent" for configurable widgets
Basic Widget Structure
import WidgetKit
import SwiftUI
struct SimpleWidget: Widget {
let kind: String = "SimpleWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SimpleWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("My Widget")
.description("Shows important information")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
Timeline Provider
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), message: "Loading...")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
let entry = SimpleEntry(date: Date(), message: "Snapshot")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0..<5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, message: "Hour \(hourOffset)")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let message: String
}
Widget View
struct SimpleWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
SmallWidgetView(entry: entry)
case .systemMedium:
MediumWidgetView(entry: entry)
case .systemLarge:
LargeWidgetView(entry: entry)
default:
Text(entry.message)
}
}
}
struct SmallWidgetView: View {
let entry: SimpleEntry
var body: some View {
VStack {
Text(entry.message)
.font(.headline)
Text(entry.date, style: .time)
.font(.caption)
}
}
}
iOS 26 Glass Presentation
Accented Rendering
Widgets on iOS 26 use the new glass presentation system:
struct GlassWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "GlassWidget", provider: Provider()) { entry in
GlassWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct GlassWidgetView: View {
let entry: SimpleEntry
var body: some View {
VStack {
Image(systemName: "star.fill")
.font(.largeTitle)
// Accented rendering for glass background
.widgetAccentedRenderingMode(.accentedDesaturated)
Text(entry.message)
.font(.headline)
}
}
}
Widget Accented Rendering Modes
// For images in widgets
Image("CustomIcon")
.widgetAccentedRenderingMode(.desaturated) // Blend with home screen
.widgetAccentedRenderingMode(.accentedDesaturated) // Blend with accent
.widgetAccentedRenderingMode(.fullColor) // Full color (media only)
Interactive Widgets
Button Actions
import AppIntents
struct InteractiveWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "Interactive", provider: Provider()) { entry in
InteractiveWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct InteractiveWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading) {
Text(entry.task.title)
.font(.headline)
Button(intent: ToggleTaskIntent(taskId: entry.task.id)) {
Label(
entry.task.isComplete ? "Completed" : "Mark Done",
systemImage: entry.task.isComplete ? "checkmark.circle.fill" : "circle"
)
}
.buttonStyle(.bordered)
}
}
}
struct ToggleTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Task"
@Parameter(title: "Task ID")
var taskId: String
func perform() async throws -> some IntentResult {
await TaskManager.shared.toggle(taskId)
return .result()
}
}
Toggle Actions
struct ToggleWidgetView: View {
let entry: SettingEntry
var body: some View {
Toggle(isOn: entry.isEnabled, intent: ToggleSettingIntent(settingId: entry.id)) {
Label("Enable Feature", systemImage: "gear")
}
.toggleStyle(.button)
}
}
Configurable Widgets
App Intent Configuration
import AppIntents
struct ConfigurableWidget: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: "ConfigurableWidget",
intent: ConfigureWidgetIntent.self,
provider: ConfigurableProvider()
) { entry in
ConfigurableWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Custom Widget")
.description("Configure which data to show")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct ConfigureWidgetIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configure Widget"
static var description = IntentDescription("Choose what to display")
@Parameter(title: "Category")
var category: WidgetCategory?
@Parameter(title: "Show Count")
var showCount: Bool
}
enum WidgetCategory: String, AppEnum {
case recent, favorites, all
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Category")
static var caseDisplayRepresentations: [WidgetCategory: DisplayRepresentation] = [
.recent: "Recent",
.favorites: "Favorites",
.all: "All"
]
}
Configurable Provider
struct ConfigurableProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> ConfigurableEntry {
ConfigurableEntry(date: Date(), configuration: ConfigureWidgetIntent())
}
func snapshot(for configuration: ConfigureWidgetIntent, in context: Context) async -> ConfigurableEntry {
ConfigurableEntry(date: Date(), configuration: configuration)
}
func timeline(for configuration: ConfigureWidgetIntent, in context: Context) async -> Timeline<ConfigurableEntry> {
let entry = ConfigurableEntry(date: Date(), configuration: configuration)
return Timeline(entries: [entry], policy: .after(Date().addingTimeInterval(3600)))
}
}
struct ConfigurableEntry: TimelineEntry {
let date: Date
let configuration: ConfigureWidgetIntent
}
Live Activities
Activity Attributes
import ActivityKit
struct DeliveryAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var status: DeliveryStatus
var estimatedArrival: Date
var driverName: String
}
var orderNumber: String
var restaurantName: String
}
enum DeliveryStatus: String, Codable {
case preparing
case onTheWay
case arriving
case delivered
}
Live Activity View
struct DeliveryActivityView: View {
let context: ActivityViewContext<DeliveryAttributes>
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(context.attributes.restaurantName)
.font(.headline)
Spacer()
Text(context.state.status.displayName)
.font(.caption)
.foregroundStyle(.secondary)
}
ProgressView(value: context.state.status.progress)
HStack {
Label(context.state.driverName, systemImage: "person.circle")
Spacer()
Text(context.state.estimatedArrival, style: .timer)
}
.font(.caption)
}
.padding()
}
}
Dynamic Island
struct DeliveryLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock screen presentation
DeliveryActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded regions
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "bag.fill")
}
DynamicIslandExpandedRegion(.trailing) {
Text(context.state.estimatedArrival, style: .timer)
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName)
.font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
ProgressView(value: context.state.status.progress)
}
} compactLeading: {
Image(systemName: "bag.fill")
} compactTrailing: {
Text(context.state.estimatedArrival, style: .timer)
} minimal: {
Image(systemName: "bag.fill")
}
}
}
}
Starting a Live Activity
func startDeliveryActivity(order: Order) async throws {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
throw ActivityError.notAuthorized
}
let attributes = DeliveryAttributes(
orderNumber: order.id,
restaurantName: order.restaurant
)
let initialState = DeliveryAttributes.ContentState(
status: .preparing,
estimatedArrival: order.estimatedArrival,
driverName: "Assigning..."
)
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil),
pushType: .token // Enable push updates
)
// Store activity ID for later updates
UserDefaults.standard.set(activity.id, forKey: "currentDeliveryActivity")
// Get push token for server updates
for await token in activity.pushTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
await sendTokenToServer(tokenString)
}
}
Updating Live Activity
func updateDeliveryStatus(to status: DeliveryStatus, driver: String? = nil) async {
guard let activityId = UserDefaults.standard.string(forKey: "currentDeliveryActivity"),
let activity = Activity<DeliveryAttributes>.activities.first(where: { $0.id == activityId })
else { return }
var newState = activity.content.state
newState.status = status
if let driver {
newState.driverName = driver
}
await activity.update(
ActivityContent(state: newState, staleDate: nil)
)
}
Ending Live Activity
func endDeliveryActivity() async {
guard let activityId = UserDefaults.standard.string(forKey: "currentDeliveryActivity"),
let activity = Activity<DeliveryAttributes>.activities.first(where: { $0.id == activityId })
else { return }
let finalState = DeliveryAttributes.ContentState(
status: .delivered,
estimatedArrival: Date(),
driverName: activity.content.state.driverName
)
await activity.end(
ActivityContent(state: finalState, staleDate: nil),
dismissalPolicy: .after(.now + 3600) // Dismiss after 1 hour
)
UserDefaults.standard.removeObject(forKey: "currentDeliveryActivity")
}
iOS 26 Live Activity Updates
CarPlay Support
Live Activities now appear on CarPlay in iOS 26:
struct CarPlayDeliveryActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock screen
DeliveryActivityView(context: context)
} dynamicIsland: { context in
// Dynamic Island config...
}
.supplementalActivityFamilies([.small]) // CarPlay support
}
}
macOS Support
Live Activities now work on macOS Tahoe:
#if os(macOS)
struct MacDeliveryActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
MacDeliveryView(context: context)
}
}
}
#endif
Control Center Widgets
Control Widget
import WidgetKit
import AppIntents
struct QuickToggleControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "QuickToggle") {
ControlWidgetToggle(
"Dark Mode",
isOn: DarkModeBinding(),
action: ToggleDarkModeIntent()
) { isOn in
Label(isOn ? "On" : "Off", systemImage: isOn ? "moon.fill" : "sun.max")
}
}
.displayName("Dark Mode")
.description("Toggle dark mode")
}
}
struct DarkModeBinding: ControlValueProvider {
var previewValue: Bool { false }
func currentValue() async throws -> Bool {
await SettingsManager.shared.isDarkMode
}
}
struct ToggleDarkModeIntent: SetValueIntent {
static var title: LocalizedStringResource = "Toggle Dark Mode"
@Parameter(title: "Dark Mode")
var value: Bool
func perform() async throws -> some IntentResult {
await SettingsManager.shared.setDarkMode(value)
return .result()
}
}
Control Widget Button
struct QuickActionControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "QuickAction") {
ControlWidgetButton(action: QuickNoteIntent()) {
Label("Quick Note", systemImage: "note.text.badge.plus")
}
}
.displayName("Quick Note")
.description("Create a quick note")
}
}
Widget Relevance (watchOS 26)
Relevance Configuration
struct RelevantWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "Relevant", provider: RelevantProvider()) { entry in
RelevantWidgetView(entry: entry)
}
.supportedFamilies([.accessoryRectangular])
}
}
struct RelevantProvider: TimelineProvider {
func relevances() async -> WidgetRelevances<Void> {
// Define when widget is most relevant
return WidgetRelevances(
// Show during workout
RelevantContext.workout: .defaultLarge,
// Show in morning
RelevantContext.date(from: morning, to: noon): .defaultMedium
)
}
// Other provider methods...
}
Widget Data Sharing
App Groups
// In both app and widget extension
let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp.shared")
sharedDefaults?.set(data, forKey: "widgetData")
// In widget
let data = UserDefaults(suiteName: "group.com.yourapp.shared")?.data(forKey: "widgetData")
Shared Container
let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.yourapp.shared"
)
Triggering Widget Refresh
import WidgetKit
// Refresh specific widget
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
// Refresh all widgets
WidgetCenter.shared.reloadAllTimelines()
Best Practices
1. Efficient Timeline Updates
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
// Generate only necessary entries
let entries = generateRelevantEntries()
// Use appropriate refresh policy
let policy: TimelineReloadPolicy
if hasUpcomingEvents {
policy = .after(nextEventDate)
} else {
policy = .atEnd
}
completion(Timeline(entries: entries, policy: policy))
}
2. Handle Widget Families
struct AdaptiveWidgetView: View {
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
CompactView()
case .systemMedium:
MediumView()
case .systemLarge, .systemExtraLarge:
DetailedView()
case .accessoryCircular:
CircularView()
case .accessoryRectangular:
RectangularView()
default:
DefaultView()
}
}
}
3. Test with Widget Development Mode
// In scheme arguments
-widgetKitDevMode YES
4. Memory Efficiency
// Load images efficiently
Image("icon")
.resizable()
.aspectRatio(contentMode: .fit)
// Avoid heavy computations in view body
// Pre-calculate in provider
Official Resources
Weekly Installs
5
Repository
bluewaves-creat…s-skillsGitHub Stars
1
First Seen
Jan 26, 2026
Installed on
opencode5
codex4
gemini-cli4
claude-code3
github-copilot3
mcpjam2