How does a WidgetKit widget work: providers, timeline entries, reload policies, families and the refresh budget?

19.4.1 · L3 · Interview

Predict first

A timeline with two hourly entries is built with each reload policy. What do the policies and the entry count read as?

import WidgetKit
import Foundation

struct E: TimelineEntry { let date: Date; let v: Int }
let now = Date()
let t1 = Timeline(entries: [E(date: now, v: 1), E(date: now.addingTimeInterval(3600), v: 2)], policy: .atEnd)
let t2 = Timeline(entries: [E(date: now, v: 1)], policy: .after(now.addingTimeInterval(900)))
let t3 = Timeline(entries: [E(date: now, v: 1)], policy: .never)
func name(_ p: TimelineReloadPolicy) -> String { p == .atEnd ? "atEnd" : p == .never ? "never" : "after" }
print(t1.entries.count, name(t1.policy), name(t2.policy), name(t3.policy))
  • 2 atEnd after never
  • 1 never after atEnd
  • 2 after atEnd never

Predict first

What are the raw values of the home-screen and accessory families?

import WidgetKit

print(WidgetFamily.systemSmall.rawValue, WidgetFamily.systemMedium.rawValue, WidgetFamily.systemLarge.rawValue, WidgetFamily.accessoryCircular.rawValue, WidgetFamily.accessoryRectangular.rawValue, WidgetFamily.accessoryInline.rawValue)
  • 0 1 2 6 7 8
  • 1 2 3 4 5 6
  • 0 1 2 3 4 5

Mental model. A widget is a billboard whose posters are printed in advance. You give the printer a roll of posters with the times to hang each, and a note saying when to send more; you cannot stand next to it repainting it, and the printer only takes so many orders a day.

A widget is a SwiftUI view rendered from a timeline, hosted in a widget extension. You define a Widget with a StaticConfiguration (or an AppIntentConfiguration for user-configurable widgets), a provider (TimelineProvider, or AppIntentTimelineProvider) and an entry type conforming to TimelineEntry (it must have a date: when this snapshot becomes current). The provider answers three calls: placeholder(in:) (a redacted sample shown while loading and in the gallery, which must return instantly), getSnapshot(in:completion:) (a representative entry for the widget gallery, fast, with sample data if real data is slow) and getTimeline(in:completion:), which returns a Timeline(entries:policy:): several entries with future dates (so the widget can change over the next hours without being woken up, such as a calendar showing the next event, or a forecast by hour) and a reload policy: .atEnd (ask again after the last entry), .after(date) (ask again at that time) or .never (until the app calls WidgetCenter.shared.reloadTimelines(ofKind:) or reloadAllTimelines()). The system renders each entry’s view at its date, in a family: .systemSmall, .systemMedium, .systemLarge, .systemExtraLarge (iPad), and the accessory families for the Lock Screen and StandBy and the watch (.accessoryCircular, .accessoryRectangular, .accessoryInline), declared with supportedFamilies and read with @Environment(\.widgetFamily) so the view adapts.

The rules that matter. The system, not you, decides when to run the provider: each widget has a daily refresh budget (roughly 40 to 70 timeline reloads per day, higher for widgets the user looks at often, and not guaranteed), so put as many future entries as you can into one timeline instead of reloading often, and do not poll. Widgets cannot run code while visible: no timers, no animations beyond a few SwiftUI transitions, no network from the view; data must be fetched in getTimeline (a short async network call is allowed within the extension’s time limit) or prepared by the app into the shared container, and the view renders from the entry. For countdowns and clocks use the system text styles (Text(date, style: .timer), .relative) that the system updates itself. Memory is tiny (tens of MB), so downsample images and keep entries small. Reload from the app when data changes meaningfully (after a sync or user action) with WidgetCenter.shared.reloadTimelines(ofKind:), which counts against the budget but is the right trigger, and tell the system a widget is relevant for the Smart Stack with TimelineEntryRelevance and relevance intents. Deep link with widgetURL(_:) or Link in medium and large families (small has one tap target), and mark private content with .privacySensitive() and redacted placeholders so the Lock Screen can hide it. Debug with the widget schemes in Xcode (timeline preview and the debug-reload of Timeline Debug), and expect behaviour on a device to differ: budgets and low-power mode throttle you. If a widget “does not update”, check the policy, the budget, whether the app reloads it, whether the provider returns quickly, and whether the extension is being killed for memory.

The first timeline has two entries and reloads after the last, the second asks to reload at a date, and the third waits for an explicit reload.

The system families are 0, 1 and 2 and the Lock Screen and watch accessory families are 6, 7 and 8.

Common traps

  • Reloading the timeline often instead of providing many entries. The refresh budget runs out and the widget goes stale; batch future entries into one timeline.
  • Doing network work or animation in the widget view. The view cannot run code while visible; fetch in the provider and render the entry.

They might follow up with

  • What does .after(date) tell the system? To ask the provider for a new timeline at about that time, subject to the budget.
  • How do you update a widget from the app? WidgetCenter.shared.reloadTimelines(ofKind:).

Related questions

  • 19.4.2 How do configurable widgets, interactive buttons and Control Center controls work with App Intents?
  • 19.1.2 How do app groups, shared containers and keychain access groups let an app and its extensions share data, and…
  • 17.6.1 How do Live Activities (ActivityKit) and WeatherKit work, and what are their limits?

How do configurable widgets, interactive buttons and Control Center… →