Building widgets with WidgetKit and SwiftUI
Learn how to create widgets for an iOS application with WidgetKit and SwiftUI.
20 Jul 2026 · 8 min read
Widgets give users a lightweight way to see useful information from an app without opening it first. A good widget is usually focused on one small job: showing today's progress, the next event, the current status, or a shortcut into a specific part of the app.
WidgetKit handles the system integration, scheduling, rendering, and configuration. We provide the data, decide when it should refresh, and build the view with SwiftUI.
Before building a widget, it helps to decide what information is useful at a glance. Widgets work best when they show a small amount of timely content or provide a shortcut to a focused action. If the user needs to make several decisions, the app itself is usually the better place for that flow.
In this article, we'll look at how to add a widget extension, configure a widget, provide timeline entries, and build the widget's SwiftUI view.
We'll use a simple quote widget as an example. The same structure can be applied to other types of widgets, such as a task summary, a weather overview, or a project status widget.

Adding a widget extension
A widget lives in its own app extension. To add one to an existing iOS app, we can use Xcode's Widget Extension template from File > New > Target and enter a name for the extension.
Xcode creates a new target with a widget entry point. The main widget type conforms to the Widget protocol and is marked with @main:
import SwiftUIimport WidgetKit@mainstruct ExampleWidget: Widget {var body: some WidgetConfiguration {// Return the widget configuration.}}
The configuration describes what kind of widget we are building, which timeline provider supplies its data, and which SwiftUI view renders the widget.
Choosing a widget configuration
Static widgets
For many widgets, StaticConfiguration is enough. It works well when the widget does not need user-configurable options.
var body: some WidgetConfiguration {StaticConfiguration(kind: "com.example.QuoteWidget",provider: ExampleTimelineProvider()) { entry inQuoteWidgetView(entry: entry)}.configurationDisplayName("Quote Widget").description("Shows the latest quote of the day.").supportedFamilies([.systemSmall, .systemMedium, .systemLarge])}
Here, kind identifies the widget, the provider supplies its timeline entries, and the closure builds the SwiftUI view. The remaining modifiers define how the widget appears when users add or edit it.
Configurable widgets
If the widget should be configurable by the user, we can use AppIntentConfiguration instead. This is useful when the widget content depends on a selected project, category, account, or any other user-provided option.
var body: some WidgetConfiguration {AppIntentConfiguration(kind: "com.example.QuoteWidget",intent: QuoteWidgetIntent.self,provider: QuoteTimelineProvider()) { entry inQuoteWidgetView(entry: entry)}.configurationDisplayName("Quote").description("Shows a quote for the selected category.")}
The configuration looks similar to the static one, but it also receives an intent type. The intent describes the values users can configure:
import AppIntentsimport WidgetKitenum QuoteCategory: String, AppEnum {case motivationcase focusstatic var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Category")static var caseDisplayRepresentations: [QuoteCategory: DisplayRepresentation] = [.motivation: "Motivation",.focus: "Focus"]}struct QuoteWidgetIntent: WidgetConfigurationIntent {static var title: LocalizedStringResource = "Quote Widget"@Parameter(title: "Category")var category: QuoteCategory = .motivation}
In this example, the user can choose between predefined quote categories when adding or editing the widget. The selected category is passed to the provider as part of the QuoteWidgetIntent configuration, so the provider can create entries for the chosen category.
For a more detailed walkthrough, check out the article on how to build a configurable widget with WidgetKit and SwiftUI.
Providing timeline entries
Widgets don't run continuously in the background. Instead, WidgetKit asks the timeline provider for entries and decides when to render them.
A timeline entry is a small model object that conforms to TimelineEntry. It must contain a date and can include any other values the widget view needs:
struct QuoteEntry: TimelineEntry {let date: Datelet quote: String}
For a static widget, the provider conforms to TimelineProvider:
struct ExampleTimelineProvider: TimelineProvider {func placeholder(in context: Context) -> QuoteEntry {QuoteEntry(date: Date(), quote: "Quote of the day")}func getSnapshot(in context: Context, completion: @escaping (QuoteEntry) -> Void) {let entry = QuoteEntry(date: Date(), quote: "Quote of the day")completion(entry)}func getTimeline(in context: Context, completion: @escaping (Timeline<QuoteEntry>) -> Void) {let entry = QuoteEntry(date: Date(), quote: "Small steps count.")let nextUpdate = Date().addingTimeInterval(60 * 60)let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))completion(timeline)}}
Let's look at the methods more closely:
- placeholder(in:) provides a lightweight representation of the widget before real data is available.
- getSnapshot(in:completion:) provides a single entry for previews and transient states.
- getTimeline(in:completion:) provides one or more entries and a reload policy.
In practice, placeholder should be generic, while snapshot can use realistic sample or current data.
The reload policy is only a request. WidgetKit decides the exact refresh timing based on system conditions, so a widget should not depend on second-by-second updates.
For a configurable widget, the idea is the same, but the provider conforms to AppIntentTimelineProvider. Its snapshot and timeline methods receive the selected configuration, which lets us use values like configuration.category when creating the entries.
Building the widget view with SwiftUI
The widget view is a regular SwiftUI view, but it should stay focused and lightweight. It receives a timeline entry and renders the current state:
struct QuoteWidgetView: View {let entry: QuoteEntryvar body: some View {Text(entry.quote).font(.headline).containerBackground(.background, for: .widget)}}
Widgets can appear in different sizes and contexts. We can use the widgetFamily environment value to adapt the layout:
struct QuoteWidgetView: View {@Environment(\.widgetFamily) private var familylet entry: QuoteEntryvar body: some View {switch family {case .systemSmall:Text(entry.quote).font(.headline)case .systemMedium:VStack(alignment: .leading) {Text("Quote").font(.caption)Text(entry.quote).font(.headline)}default:Text(entry.quote)}}}
When possible, keep the view resilient. The widget might render with placeholder data, stale data, or in a smaller family than expected.
Because the widget view is built with SwiftUI, we can preview different entries and widget families in Xcode while adjusting the layout.
Opening the app from a widget
The simplest interaction is opening the app when the user taps the widget. To open a specific screen, the widget needs a URL that the app can handle. This can be a custom URL scheme like myapp://quote/today, or a universal link if the same destination should also open from outside the app.
For a single destination, we can attach a URL to a view in the widget hierarchy:
Text(entry.quote).widgetURL(URL(string: "myapp://quote/today"))
In SwiftUI, the app can handle the URL with onOpenURL(perform:) and route the user to the matching screen:
@mainstruct ExampleApp: App {var body: some Scene {WindowGroup {ContentView().onOpenURL { url in// Route to the matching screen.}}}}
For widgets that show multiple tappable areas, we can use Link:
Link(destination: URL(string: "myapp://quote/today")!) {Text(entry.quote)}
Adding interactive controls
On newer OS versions, widgets can also support simple interactions without launching the app. For example, a button can run an AppIntent to update shared app data.
Interactive widgets are best suited for small, predictable actions. If an action requires a longer flow, authentication, or more context, opening the app is usually the better experience.
Sharing data with the widget extension
Because the widget runs in a separate extension, it needs access to the data it displays. For simple values, this can be done with shared UserDefaults in an App Group. For larger or structured data, we can use a shared container or another persistence setup that both the app and widget extension can access.
Reloading widget timelines
When app data changes, we can ask WidgetKit to reload the widget's timeline:
WidgetCenter.shared.reloadTimelines(ofKind: "com.example.QuoteWidget")
To reload all timelines for the app's widgets, we can call:
WidgetCenter.shared.reloadAllTimelines()
This is useful after saving data that is displayed in the widget. Still, the system remains in control of the final refresh timing, so the widget should always display a reasonable state until the next update happens.
Summary
To build a WidgetKit widget, we create a widget extension, choose a configuration, provide timeline entries, and render the current entry with SwiftUI.
For simple widgets, StaticConfiguration and TimelineProvider are enough. For user-configurable widgets, AppIntentConfiguration and AppIntentTimelineProvider provide a modern way to model the configuration. From there, we can decide whether tapping the widget should open the app, or whether a small action can be handled directly with an AppIntent.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



