Skip to content
advanced Phase 7 · Advanced UI

WidgetKit

Build home screen and lock screen widgets with WidgetKit, timelines, and interactive controls.

55m
3 problems
Topic Progress 0%

Widget Basics

Creating Your First Widget

WidgetKit widgets are SwiftUI views that display glanceable information on the Home Screen, Lock Screen, and StandBy mode. A widget is defined by conforming to the Widget protocol.

import WidgetKit
import SwiftUI

struct SimpleWidget: Widget {
    let kind: String = "SimpleWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: SimpleProvider()) { entry in
            SimpleWidgetView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("A simple widget example.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Widget Entry View

The entry view displays the widget content. It receives a timeline entry with the data to display.

struct SimpleEntry: TimelineEntry {
    let date: Date
    let title: String
    let count: Int
}

struct SimpleWidgetView: View {
    let entry: SimpleEntry
    @Environment(\.widgetFamily) var family
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(entry.title)
                .font(.headline)
            
            Text("Count: \(entry.count)")
                .font(.title)
                .fontWeight(.bold)
            
            Text(entry.date, style: .time)
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

Widget Bundles

Use WidgetBundle to group multiple widgets together for a unified widget gallery experience.

@main
struct MyAppWidgets: WidgetBundle {
    var body: some Widget {
        SimpleWidget()
        WeatherWidget()
        CalendarWidget()
    }
}

Widget Families

Widgets come in different sizes determined by the widgetFamily environment value:

  • .systemSmall - Small square widget
  • .systemMedium - Medium rectangle widget
  • .systemLarge - Large rectangle widget
  • .systemExtraLarge - Extra large (iPad only)
  • .accessoryCircular - Lock Screen circular
  • .accessoryRectangular - Lock Screen rectangular
  • .accessoryInline - Lock Screen inline text

Timelines

Understanding TimelineProvider

The TimelineProvider protocol defines when and how your widget updates. It provides entries at specific times and tells WidgetKit when to refresh.

struct SimpleProvider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), title: "Loading...", count: 0)
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        let entry = SimpleEntry(date: Date(), title: "Snapshot", count: 42)
        completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
        let now = Date()
        let entry = SimpleEntry(date: now, title: "Current", count: getUserCount())
        
        // Refresh every 30 minutes
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: now)!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
        completion(timeline)
    }
}

Timeline Policies

Control when your widget refreshes using timeline policies:

// Refresh at a specific time
let timeline = Timeline(entries: entries, policy: .atSpecificDate(refreshDate))

// Refresh after a duration
let timeline = Timeline(entries: entries, policy: .after(futureDate))

// Never auto-refresh (use .after with far future date)
let timeline = Timeline(entries: entries, policy: .never)

Reloading Timelines

Trigger widget updates from your app using WidgetCenter.

import WidgetKit

// Reload all widgets of a specific kind
WidgetCenter.shared.reloadTimelines(ofKind: "SimpleWidget")

// Reload all widgets
WidgetCenter.shared.reloadAllTimelines()

// Reload specific widget by identifier
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

Handling Widget Errors

Provide fallback content for when data is unavailable.

struct FallbackView: View {
    var body: some View {
        VStack {
            Image(systemName: "exclamationmark.triangle")
            Text("Unable to load data")
                .font(.caption)
        }
    }
}

// In your entry view
struct MyWidgetView: View {
    let entry: SimpleEntry
    var body: some View {
        if entry.isPlaceholder {
            FallbackView()
        } else {
            // Normal content
        }
    }
}

Interactive Widgets

App Intents for Widget Interaction

Starting with iOS 17, widgets can respond to user taps and perform actions using App Intents.

import AppIntents

struct IncrementIntent: AppIntent {
    static var title: LocalizedStringResource = "Increment Counter"
    static var description = IntentDescription("Increases the widget counter.")
    
    @Parameter(title: "Widget ID")
    var widgetID: String
    
    func perform() async throws -> some IntentResult {
        let count = UserDefaults(suiteName: "group.com.myapp")?.integer(forKey: "count") ?? 0
        UserDefaults(suiteName: "group.com.myapp")?.set(count + 1, forKey: "count")
        WidgetCenter.shared.reloadTimelines(ofKind: widgetID)
        return .result()
    }
}

Adding Buttons to Widgets

Use Button(intent:) in your widget view to trigger App Intents.

struct InteractiveWidgetView: View {
    let entry: SimpleEntry
    
    var body: some View {
        VStack {
            Text("Count: \(entry.count)")
                .font(.largeTitle)
            
            HStack {
                Button(intent: IncrementIntent(widgetID: "CounterWidget")) {
                    Image(systemName: "plus.circle.fill")
                }
                
                Button(intent: DecrementIntent(widgetID: "CounterWidget")) {
                    Image(systemName: "minus.circle.fill")
                }
            }
        }
        .padding()
    }
}

Widget Links and Deep Linking

Link widget taps to specific screens in your app using Link or widget URLs.

struct DeepLinkWidgetView: View {
    let entry: SimpleEntry
    
    var body: some View {
        VStack {
            Text(entry.title)
            
            // Open specific URL in app
            Link(destination: URL(string: "myapp://detail/\(entry.id)")!) {
                Text("View Details")
                    .font(.caption)
            }
        }
    }
}

Live Activities

Live Activities show real-time updates on the Lock Screen and Dynamic Island.

struct LiveActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            LockScreenLiveActivityView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Image(systemName: "delivery")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("\(context.state.eta) min")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    DeliveryProgressView(progress: context.state.progress)
                }
            }
        }
    }
}

Quiz

1. What protocol defines when a widget updates?

Question 1 options

2. How do you group multiple widgets together?

Question 2 options

3. What does the .systemMedium family provide?

Question 3 options

4. How do you trigger a widget refresh from your app?

Question 4 options

Flashcards

Question

What is a TimelineProvider?

Answer

A protocol that provides timeline entries at specific times and controls when WidgetKit refreshes widget content.

Question

What are the widget families available?

Answer

systemSmall, systemMedium, systemLarge, systemExtraLarge, accessoryCircular, accessoryRectangular, accessoryInline.

Question

How do you make widgets interactive in iOS 17+?

Answer

Use AppIntents with Button(intent:) to trigger actions when users tap widget buttons.

Question

What is a WidgetBundle?

Answer

A container that groups multiple widget definitions for a unified widget gallery experience in your app.

Revision Notes

Key Takeaways

  • 1. TimelineProvider controls when and how widgets update
  • 2. WidgetBundle groups multiple widgets together
  • 3. Widget families determine widget size and layout
  • 4. AppIntents enable interactive widgets in iOS 17+
  • 5. WidgetCenter triggers widget refreshes from the app

Interview Tips

  • Explain the difference between getSnapshot and getTimeline
  • Discuss how to design for different widget families
  • Describe how to use AppIntents for widget interactions

Cheat Sheet

WidgetKit Quick Reference

  • Widget protocol - Define a widget with configuration
  • TimelineProvider - Provides entries and refresh schedule
  • StaticConfiguration - Simple widget configuration
  • IntentConfiguration - Widget with App Intents
  • WidgetBundle - Group multiple widgets
  • WidgetCenter.shared.reloadTimelines() - Trigger refresh
  • @Environment(.widgetFamily) - Detect widget size
  • supportedFamilies() - Restrict widget sizes