What should a timeline provider return when the fetch fails, and what do the reload policies mean as values?
Predict first
What do the success and the failure timelines contain?
import WidgetKit
struct Entry: TimelineEntry { let date: Date; let text: String }
enum Fetch: Error { case offline }
func build(now: Date, fetch: () throws -> [String], lastGood: String?) -> Timeline<Entry> {
do {
let items = try fetch()
let entries = items.enumerated().map { Entry(date: now.addingTimeInterval(Double($0.offset) * 3600), text: $0.element) }
return Timeline(entries: entries, policy: .atEnd)
} catch {
let stale = Entry(date: now, text: (lastGood ?? "No data") + " (stale)")
return Timeline(entries: [stale], policy: .after(now.addingTimeInterval(900)))
}
}
let now = Date(timeIntervalSince1970: 0)
let ok = build(now: now, fetch: { ["a", "b", "c"] }, lastGood: nil)
print(ok.entries.map { "\(Int($0.date.timeIntervalSince1970)):\($0.text)" }, ok.policy)
let bad = build(now: now, fetch: { throw Fetch.offline }, lastGood: "21°")
print(bad.entries.map { "\(Int($0.date.timeIntervalSince1970)):\($0.text)" }, bad.policy)
- Three hourly entries with .atEnd (date nil); then one stale entry "21° (stale)" with an after-15-minutes date
- Three entries in both cases
- The failure case returns an empty timeline
Mental model. A newspaper that cannot get today’s edition prints yesterday’s front page with a “last updated” stamp and tries again soon, rather than leaving the stand empty.
A provider must always complete, because a widget that never receives a timeline shows stale or placeholder content with no explanation. So build the timeline in a function that never throws: on success return several future-dated entries and .atEnd; on failure return one entry made from the last good data, marked as stale, and .after(date) a short time ahead (15 minutes in the check) so the system asks again. The policies are plain values: the check prints .atEnd as date: nil and .after(t) as that date. These are requests, not promises: the system still applies its daily budget (Q406), so a retry shortly after a failure can be refused.
Keep the last good snapshot in the shared container (Q402), written by the app or by the previous successful provider run, and read it in the fallback; an error entry with no data is the worst experience. Do not loop retries inside getTimeline; one short attempt, then fall back. Show staleness honestly in the view (“updated 2h ago” with Text(date, style: .relative)) rather than presenting old numbers as current. Not run here: how often the system actually honours .after on a device, which depends on budget and power state.
The fallback builds one stale entry from the last good value and schedules a retry; success batches future entries.
Common traps
- Throwing or never calling the completion on failure. The widget keeps showing whatever it had, with no retry scheduled.
- Retrying in a loop inside the provider. It spends the time limit and the budget; return a fallback and a later policy.
They might follow up with
- What does
.neverstore? In the check it is a date far in the future (the year 4001), so “never” is a distant reload date, and the app’sreloadTimelinesis the real trigger.
Related questions
- 19.4.1 How does a WidgetKit widget work: providers, timeline entries, reload policies, families and the refresh…
- 19.1.2 How do app groups, shared containers and keychain access groups let an app and its extensions share data, and…
- 19.8.2 How do you unit test a widget’s timeline logic when the provider’s Context cannot be created in a test?
How do you unit test a widget’s timeline logic when the provider’s… →