widgets-live-activities
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
More from bluewaves-creations/bluewaves-skills
photographer-testino
Generate images in Mario Testino's glamorous vibrant style. Use when users ask for Testino style, high fashion glamour, bold saturated colors, warm luxurious photography, dynamic sensual energy.
35photographer-lindbergh
Generate images in Peter Lindbergh's iconic black and white style. Use when users ask for Lindbergh style, raw authentic beauty, emotional B&W portraits, supermodel aesthetic, or unretouched natural photography.
30photographer-lachapelle
Generate images in David LaChapelle's surreal pop art style. Use when users ask for LaChapelle style, pop surrealism, hyper-saturated colors, theatrical staging, baroque maximalism, kitsch aesthetic.
24epub-creator
Create production-quality EPUB 3 ebooks from markdown and images with automated QA, formatting fixes, and validation. Use when creating ebooks, converting markdown to EPUB, or compiling chapters into a publishable book. Handles markdown quirks, generates TOC, adds covers, and validates output automatically.
22photographer-vonunwerth
Generate images in Ellen von Unwerth's playful vintage style. Use when users ask for von Unwerth style, playful sensuality, vintage film noir, whimsical feminine photography, retro glamour, narrative storytelling.
19photographer-ritts
Generate images in Herb Ritts' sculptural black and white style. Use when users ask for Ritts style, classical Greek aesthetic, sculptural body photography, California golden hour, minimalist athletic portraits.
18