WWDC.ai

WidgetKit foundations

Learn WidgetKit fundamentals: build SwiftUI widgets with timelines, reload policies, app integration, supported families, and adaptive styling.

Watch on Apple Developer

TL;DR

  • Widgets are SwiftUI views supplied by a widget extension; the app and extension run separately, so shared data should use an app group container such as shared UserDefaults or a database.
  • WidgetKit keeps widgets current with timelines made of TimelineEntry values; choose .atEnd, .afterDate, or .never reload policies based on how predictable your content updates are.
  • Use deep links, AppIntent-based configuration, and interactive buttons or toggles to connect widgets back to app content and actions.
  • Test widgets across supported families, full color/tinted/clear rendering modes, local devices, macOS remote widgets, SwiftUI previews, and WidgetKit developer mode.

What makes a good widget

Widgets extend app content across iOS, iPadOS, watchOS, visionOS, macOS, CarPlay, and remote widget placements. The session frames effective widgets around three qualities: glanceable, relevant, and personalizable.

Glanceable widgets communicate useful information quickly. Relevant widgets update for time, personal patterns, or location. Personalizable widgets let people choose the content that matters to them.

  • Build widgets with WidgetKit and SwiftUI, regardless of whether the containing app uses SwiftUI or UIKit.
  • Use widgets for concise, high-value content rather than duplicating an entire app screen.
  • Support multiple placements and sizes where the content still makes sense.

Widget extension, configuration, and view structure

Apps expose widgets through a widget extension. The extension runs as a separate process from the app, so data shared between the app and widgets should be stored in a shared container configured with an app group.

A widget declares a WidgetConfiguration. Use StaticConfiguration when the widget configures itself from app state. Use AppIntentConfiguration when people should be able to customize the widget. The configuration supplies a unique kind, a timeline provider, and a SwiftUI view builder for each timeline entry.

Use containerBackground(for: .widget) to identify the widget background. This lets the system replace the background with an adaptive glass material in tinted or clear environments.

  • The widget extension is focused on providing widget data and views to WidgetKit.
  • Timeline entries contain the data needed to render a widget view at specific dates.
  • The rendered widget views are archived and displayed by the system.

Static widget configuration with a widget background

A simple widget uses StaticConfiguration, a TimelineProvider, and a SwiftUI view for each entry, with containerBackground marking the background for system adaptation.

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(
                book: entry.book,
                message: entry.message,
                timeOfDay: entry.timeOfDay
            )
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
        }
    }
}

Timelines and reload policies

WidgetKit asks the extension for a timeline: a series of timeline entries, each representing the data to display at a particular time. The timeline provider also supplies a snapshot for previews in the widget gallery and a placeholder for instant loading before content is available.

Placeholders should be synchronous and avoid disk or network work. The example uses a redacted version of the view. Snapshots should be realistic and make a strong first impression, even if the user has not yet created app data.

Reload policy is central to keeping a widget relevant while respecting system update budgets. WidgetKit may throttle frequent reloads, especially while the app is foregrounded, so provide multiple timeline entries whenever possible and consider a final reload when the app enters the background if data changed.

  • Use .atEnd when the system should reload after all entries are exhausted.
  • Use .afterDate when there is a known future time when the timeline should be recalculated, such as the end of the day.
  • Use .never when automatic reloads do not make sense; then reload explicitly with WidgetCenter reload APIs or a push notification.
  • For highly ephemeral, frequently updating, alert-capable content with a defined start and end, consider Live Activities instead of a widget.

Families and placement

Widgets come in multiple families and placements. The recommendation is to support as many sizes as are appropriate, while starting with a few families can keep the initial implementation simple.

The same widget and timeline provider can often be reused across families while providing SwiftUI layouts that fit each family's shape and size. The session highlights systemExtraLargePortrait, introduced in visionOS 26 and newly available in macOS, iOS, and iPadOS 27.

  • Use .supportedFamilies to declare which widget families a widget supports.
  • Do not support families where the content does not work well.
  • iOS widgets can also appear on CarPlay and as remote widgets on macOS, so interactions and layouts should be tested in those contexts.

Restrict supported widget families

Declare the families a widget supports; add more families when the data and layout work for those sizes.

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(
                book: entry.book,
                message: entry.message,
                timeOfDay: entry.timeOfDay
            )
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
        }
        .supportedFamilies([.systemMedium])
    }
}

Integrating widgets with app content

WidgetKit provides three main integration paths: deep links, configurable widgets, and interactive elements. The default tap behavior opens the app, but widgets that display specific content should route directly to the corresponding destination.

Configurable widgets use App Intents so people can personalize widget content, such as selecting a location or a book. Keep configuration fast, limit it to one or two parameters when possible, and provide sensible defaults so configuration is not required up front.

Interactive widgets expose buttons or toggles. Because widget views are archived and app code is not running while the widget is onscreen, interactive controls execute an App Intent on behalf of the widget.

  • Use widgetURL for whole-widget deep links into app-specific content.
  • Use AppIntentConfiguration when the widget needs user-selected configuration.
  • Use buttons and toggles for the most important lightweight actions from the app.

widgetURL lets a tap open the app directly to relevant content, such as a book detail page encoded by ID.

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(
                book: entry.book,
                message: entry.message,
                timeOfDay: entry.timeOfDay
            )
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
            .widgetURL(URL(string: "bookclub://reading/\(book.bookID)"))
        }
        .supportedFamilies([.systemMedium])
    }
}

Adapting and testing system appearance

Widgets adapt to full color, tinted, and clear system customizations. In tinted or clear modes, the system renders widget content through a glass material and replaces the declared background with an adaptive effect.

SwiftUI handles much of this automatically, but custom content may need explicit rendering behavior. The example book cover image rendered incorrectly in an accented environment until the image was marked to remain full color.

Testing should cover all environments where the widget appears. Use local devices, SwiftUI previews, WidgetKit developer mode, and macOS remote widget testing. Developer mode lifts constraints such as reload budgets to speed iteration.

  • Test full color, tinted, and clear rendering modes.
  • Check different families, color schemes, and rendering modes in the Xcode SwiftUI canvas.
  • Verify widget interactions still feel right when an iOS widget appears as a remote widget on Mac.

Preserve full-color image rendering in accented mode

Use widgetAccentedRenderingMode(.fullColor) for imagery, such as book covers, that should keep its original colors in accented widget rendering.

struct BookCoverImage: View {
    let imageName: String

    var body: some View {
        Image(imageName, bundle: .main)
            .widgetAccentedRenderingMode(.fullColor)
    }
}
Unofficial, not associated with Apple. BySuperwall

On this page

Ask AI