← All notes
Native iOS Development7 min

A reload budget is not a live feed

A home-screen widget looks like a miniature live view of the app. It is not. WidgetKit runs your extension briefly, paints a snapshot, and walks away. Apple budgets roughly forty to seventy reloads per day for a widget people actually look at — and far fewer for ones they ignore. Treating that budget like a real-time feed is how RuleCaddie- and Veto-shaped surfaces go stale at the exact moment someone glances at them.

The product promise has to survive fifteen quiet minutes

RuleCaddie's home-screen widget exists so a common relief procedure is reachable without unlocking into the full rules app. Veto's widget exists so you notice a round is waiting on you without opening the tab. Neither promise requires second-by-second freshness. Both break if the surface still shows yesterday's default after the underlying state changed once and you burned the day's reloads on noise.

That is the design cut. Ask what can be wrong for half an hour without teaching the user to ignore the widget. A temperature can drift. A "your turn" badge cannot quietly lie. A ruling tip that rotates on a predictable schedule can be precomputed. A score that only updates when the app syncs should not pretend to poll.

If the honest answer is that the content must move continuously, you are describing a Live Activity or the app itself — not a widget. Shipping a feed-shaped widget on a budget-shaped API produces the worst failure mode: it looks alive in the gallery and goes quiet in the field.

Prebuild the timeline; do not poll your way through the day

When the next states are knowable, write them into the timeline up front. A widget that surfaces three common Rules of Golf procedures can schedule entry changes across the round instead of asking WidgetKit for a new timeline every few minutes. Entries should be at least about five minutes apart; tighter spacing does not buy you precision the system will honor.

Pick the reload policy that matches reality. Use after(date) when you know the next meaningful change. Use atEnd when the timeline will simply run dry. Use never for data-driven surfaces and call WidgetCenter.shared.reloadTimelines(ofKind:) from the app when the shared store actually changes. The common anti-pattern is reloadAllTimelines() on every foreground transition — that spends budget whether anything relevant moved or not.

Placeholder and snapshot paths stay synchronous and cheap. Hardcoded sample data for the gallery is correct. A network round-trip inside getSnapshot is how the gallery feels broken and how you discover, late, that the extension's memory and time limits are not the same as the app's.

Share a store, not a hope that the extension stays awake

The widget extension is a separate process. It does not see your app's memory. The boring bridge is an App Group: a small shared container the app writes when state changes, and the TimelineProvider reads when WidgetKit asks for entries. UserDefaults(suiteName:) is enough for a tip id, a waiting-round count, or a last-sync stamp. Files in the group container cover larger payloads.

Write the store before you ask for a reload. Calling reloadTimelines and then writing the file is a race you will lose under time pressure. The provider should be able to produce a correct timeline from disk alone — including after a reboot when your app has not launched yet.

Interactive buttons via App Intents are the exception that does not spend the budget the same way: a tap runs perform() and WidgetKit requests a fresh timeline afterward. Keep perform() tiny — flip a flag in the shared store, enqueue work for the app, return. Heavy network or model calls inside the extension steal the interaction and still leave you with a snapshot, not a session.

Budget honesty is a shipping gate

WidgetKit Developer Mode lifts the budget so iteration is possible. That mode is not the product. Before release, turn it off and live with the widget for a day on a device that is not on your desk charging. Watch whether the surface you care about still updates after lunch, after a round, after the user has not opened the app.

Configurable widgets multiply the trap. Two instances each get their own budget, but your app can still waste both by reloading every kind whenever any push arrives. Check getCurrentConfigurations before reloading, and reload only the kind — ideally only when a configured instance cares about the payload.

Complications and watch companions inherit the same discipline at a smaller canvas. If the watch face only has room for a count or a short label, do not spend a reload pipeline feeding it paragraphs the face will never show. Edit the content until it fits the budget and the pixels, then stop.

What we treat as done

A widget ships when its timeline policy matches how often the data actually changes, the shared store is the single source the provider reads, and a day of real-device use with Developer Mode off still shows the states you promised. Gallery polish without that gate is a screenshot, not a shipping surface.

The rule we keep coming back to on RuleCaddie and Veto: design for the glance that happens when the app is cold and the budget is half spent. If that glance is still honest, the reload strategy is working. If it only looks right while you are actively debugging reloads, you built a live feed on an API that was never one.

Questions

How often can a WidgetKit widget actually refresh?
Apple budgets on the order of 40–70 reloads per day for a frequently viewed widget, tuned to how often it is visible. That is not a polling interval you control — plan timelines and explicit reloads around real state changes.
When should we use TimelineReloadPolicy.never?
When updates are event-driven rather than clock-driven. Write new state into an App Group store, then call reloadTimelines(ofKind:) from the app. Avoid reloadAllTimelines on every foreground as a substitute for knowing what changed.
What belongs in the widget versus a Live Activity?
Widgets are stale-tolerant glances with a daily reload budget. If the user needs continuous, time-bounded updates — a live match, an active round timer — that is Live Activity territory, not a tighter widget poll.

Sources

  1. Apple — Keeping a widget up to date
  2. Apple — WidgetCenter
  3. Apple — App Intents
  4. RuleCaddie case study

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us