RORK LABJP
MAX — Rork Max is built on Claude Code and Claude Opus 4.6, generating native Swift apps directly instead of React NativeAPPLE — Rork Max targets the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageWORKFLOW — In practice, users settle into letting the AI scaffold while they rewrite the state management and data layer themselvesSEED — Rork raised a $15M seed led by Left Lane Capital in April, with Peak XV, True Ventures, and a16z Speedrun joiningPAPERLINE — Rork acquired app builder Paperline and says it will stay acquisitive to bring in engineering talentREVIEW — Three-month revisit reviews are growing, clarifying where the tool shines and where it doesn'tMAX — Rork Max is built on Claude Code and Claude Opus 4.6, generating native Swift apps directly instead of React NativeAPPLE — Rork Max targets the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageWORKFLOW — In practice, users settle into letting the AI scaffold while they rewrite the state management and data layer themselvesSEED — Rork raised a $15M seed led by Left Lane Capital in April, with Peak XV, True Ventures, and a16z Speedrun joiningPAPERLINE — Rork acquired app builder Paperline and says it will stay acquisitive to bring in engineering talentREVIEW — Three-month revisit reviews are growing, clarifying where the tool shines and where it doesn't
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.

WidgetKit10TimelineProvideriOS109Rork Max223WidgetsPerformance23

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

App Dev2026-07-03
Making Your Rork Max App Resilient to Dropped and Restored Connections: Offline Detection and Retry with NWPathMonitor
Build networking that survives a lost signal in your Rork Max native Swift app with NWPathMonitor. Detect offline states, respect Low Data Mode and cellular, and auto-resend queued work on reconnect — all with working Swift code.
App Dev2026-07-03
Keeping Downloads Alive After Your Rork Max App Is Killed: Background URLSession Design and Relaunch Handling
How to design downloads in a Rork Max native Swift app so transfers continue in the OS daemon even after the app is suspended or terminated. Covers relaunch wiring, resumeData recovery, and measured isDiscretionary behavior with working code.
App Dev2026-07-01
Building SharePlay in Rork Max's Native Swift — Keeping Two Screens in the Same State
Implementation notes on building SharePlay with GroupActivities in Rork Max's native Swift — moving two screens through the same state over FaceTime. Covers declaring the GroupActivity, joining a GroupSession, syncing state with GroupSessionMessenger, handling latency and conflicts, catching up late joiners, and the boundary for bridging from React Native, with the pitfalls I actually hit.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →