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?
2. How do you group multiple widgets together?
3. What does the .systemMedium family provide?
4. How do you trigger a widget refresh from your app?
Flashcards
Question
What is a TimelineProvider?
Click to reveal answer
Answer
A protocol that provides timeline entries at specific times and controls when WidgetKit refreshes widget content.
Question
What are the widget families available?
Click to reveal answer
Answer
systemSmall, systemMedium, systemLarge, systemExtraLarge, accessoryCircular, accessoryRectangular, accessoryInline.
Question
How do you make widgets interactive in iOS 17+?
Click to reveal answer
Answer
Use AppIntents with Button(intent:) to trigger actions when users tap widget buttons.
Question
What is a WidgetBundle?
Click to reveal answer
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