◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 26 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 26 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/App Dev
◇ App Dev/2026-06-14Advanced

Designing WidgetKit Timelines Around the Refresh Budget: Why My Wallpaper Widgets Stopped

Why does a home screen widget stop updating after the evening? A clear look at WidgetKit timeline design through three lenses: the refresh budget, the reload policy, and entry density. With a working TimelineProvider, an entry design that does not burn through the budget, and relevance-based prioritization, drawn from running six wallpaper apps solo.

WidgetKit11TimelineProvideriOS118Rork Max235WidgetsPerformance25

✦ Premium Article

Running six wallpaper apps solo as an indie developer, I started getting more reports that "the home screen widget's photo of the day stays yesterday's after the evening." It switched correctly in the simulator, but on real devices it froze by evening. The thing creating that gap was an invisible mechanism: WidgetKit's refresh budget.

Unlike an app, a widget cannot redraw itself whenever and as often as it likes. The system assigns a rough daily budget for the number of refreshes, and once you spend it, no updates arrive until the next day. Build a timeline without knowing this and you get exactly this symptom — the budget is eaten in the morning, and the afternoon goes silent.

A timeline is a "schedule of the future"

The center of WidgetKit's model is that a widget does not get asked "what do you show right now" every time; instead it submits "the display schedule for a while ahead" all at once. The Timeline returned by a TimelineProvider is a schedule of several TimelineEntry values, each stamped with a time.

The system follows that schedule and switches to the next entry automatically when its time arrives. In other words, if you compute future displays ahead of time and hand them over in a batch, no queries to the system (no budget spend) happen in between. Grasp this and the design direction is set.

Emitting one entry at a time drains the budget

The first thing I did wrong was return a single entry per hour and repeat with .atEnd, meaning "come back when this one is done." It appears to work, but because every reload spends budget, on an eventful day the budget runs out in the early afternoon.

The right approach is to return several future entries in a single timeline generation. Compute, say, a full day of switches as 24 entries up front, pack them into one Timeline, and you coast through the day with almost no extra queries.

import WidgetKit
import SwiftUI
 
struct WallpaperProvider: TimelineProvider {
    func placeholder(in context: Context) -> WallpaperEntry {
        WallpaperEntry(date: Date(), imageName: "placeholder")
    }
 
    func getSnapshot(in context: Context, completion: @escaping (WallpaperEntry) -> Void) {
        completion(WallpaperEntry(date: Date(), imageName: todaysImageName()))
    }
 
    func getTimeline(in context: Context, completion: @escaping (Timeline<WallpaperEntry>) -> Void) {
        var entries: [WallpaperEntry] = []
        let calendar = Calendar.current
        let now = Date()
 
        // Generate 12 future entries at 2-hour steps and return them batched
        for hourOffset in stride(from: 0, to: 24, by: 2) {
            guard let entryDate = calendar.date(byAdding: .hour, value: hourOffset, to: now) else { continue }
            let name = imageName(for: entryDate)
            entries.append(WallpaperEntry(date: entryDate, imageName: name))
        }
 
        // Come back for the next timeline only once, at the start of tomorrow
        let tomorrow = calendar.date(byAdding: .day, value: 1, to: now)!
        completion(Timeline(entries: entries, policy: .after(tomorrow)))
    }
}

The crux of this is the for loop that builds the future entries in a batch, and the policy set to .after(tomorrow), declaring "the next pickup can wait until tomorrow." That dramatically cut the daily query count and the evening silence disappeared.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Take home a working Swift TimelineProvider that batches future entries instead of emitting one at a time, so you do not exhaust the daily refresh budget
✦Confirm in a table which of .atEnd, .after and .never to pick for which kind of widget
✦Understand the cause and fix for two symptoms I actually hit as an indie developer: updates dying in the evening, and a stale image after a tap
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-04-05
Rork Max × WidgetKit & Live Activities in Practice — From Home Screen to Dynamic Island
A complete guide to implementing iOS Widgets, Lock Screen widgets, and Dynamic Island Live Activities with Rork Max. Covers WidgetKit fundamentals, Timeline update strategies, App Intents integration, and monetization.
◇ App Dev2026-10-26
Linking.canOpenURL Keeps Returning false on iOS Only — Declaring LSApplicationQueriesSchemes, and the 25-Entry Cap Coming With iOS 27
When Linking.canOpenURL returns false on iOS no matter what is installed, the cause is usually a missing Info.plist declaration. Here is how to fix it in Expo's app.json, what Android needs instead, and how to prepare for the 25-entry limit for apps linked on iOS 27.
◇ App Dev2026-10-15
When adjustsFontSizeToFit Shrinks Just One Row, Suspect the Box Height, Not the String
Only the 10th row of a list drops to 4pt on iOS, though the data and strings are fine. I read the Yoga rounding and font-fit sources, then counted how often the box comes up short in Node.