A widget's process isn't alive to redraw itself on a timer — WidgetKit renders from a schedule of future states you hand it up front, then the system decides when to actually swap the next one in.
#The new way
import WidgetKit
import SwiftUI
struct StepCountEntry: TimelineEntry {
let date: Date
let steps: Int
}
struct StepCountProvider: TimelineProvider {
func placeholder(in context: Context) -> StepCountEntry {
StepCountEntry(date: .now, steps: 0)
}
func getSnapshot(in context: Context, completion: @escaping (StepCountEntry) -> Void) {
completion(StepCountEntry(date: .now, steps: 4200))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<StepCountEntry>) -> Void) {
let now = Date()
let entries = (0..<4).map { hour in
StepCountEntry(date: now.addingTimeInterval(Double(hour) * 3600), steps: 4200 + hour * 500)
}
completion(Timeline(entries: entries, policy: .atEnd))
}
}
struct StepCountWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "StepCount", provider: StepCountProvider()) { entry in
Text("\(entry.steps) steps")
}
.configurationDisplayName("Step Count")
.description("Shows your step count throughout the day.")
}
}
#Why it matters
- The system, not your app, decides exactly when to render each entry — the widget process doesn't need to be running
- A single
getTimelinecall can queue up hours of future states, so the UI updates smoothly without repeated wakeups TimelineReloadPolicy(.atEnd,.after(date),.never) controls when WidgetKit asks your provider for a fresh timeline- Battery cost is bounded because rendering happens on a schedule you control, not continuously
#Gotcha
Reload budgets are real but not documented as a fixed number, and they're not guaranteed — WidgetKit throttles how often it'll actually call getTimeline again based on the widget's visibility and a system-wide budget, so an .atEnd policy on a short timeline can end up refreshing far less often than the entries suggest. Calling WidgetCenter.shared.reloadTimelines(ofKind:) from the app doesn't bypass this either; it's still subject to the same budget. Design timelines assuming refreshes will be less frequent than requested, and verify current budget behavior against Apple's docs rather than a fixed number, since it has changed across iOS releases.