●MAX — Rork Max is built on Claude Code and Claude Opus 4.6, generating native Swift apps directly instead of React Native●APPLE — Rork Max targets the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●WORKFLOW — In practice, users settle into letting the AI scaffold while they rewrite the state management and data layer themselves●SEED — Rork raised a $15M seed led by Left Lane Capital in April, with Peak XV, True Ventures, and a16z Speedrun joining●PAPERLINE — Rork acquired app builder Paperline and says it will stay acquisitive to bring in engineering talent●REVIEW — Three-month revisit reviews are growing, clarifying where the tool shines and where it doesn't●MAX — Rork Max is built on Claude Code and Claude Opus 4.6, generating native Swift apps directly instead of React Native●APPLE — Rork Max targets the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●WORKFLOW — In practice, users settle into letting the AI scaffold while they rewrite the state management and data layer themselves●SEED — Rork raised a $15M seed led by Left Lane Capital in April, with Peak XV, True Ventures, and a16z Speedrun joining●PAPERLINE — Rork acquired app builder Paperline and says it will stay acquisitive to bring in engineering talent●REVIEW — Three-month revisit reviews are growing, clarifying where the tool shines and where it doesn't
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.
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 WidgetKitimport SwiftUIstruct 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.
A Timeline's policy has three options, and which you choose for which widget changes the behavior.
.atEnd comes back for more once the submitted entries are used up. Chopping the schedule short triggers frequent reloads, so it suits only cases where you can prepare entries well into the future.
.after(date) comes back at a specified time. For wallpaper or calendar widgets that should update at the day boundary or at fixed times, this was the most budget-efficient choice.
.never does not update at all until the app explicitly requests a reload. Use it when you only want to refresh on user action or push receipt. In my setup I also choose .never in places where I explicitly call WidgetCenter.shared.reloadTimelines(ofKind:) on a settings change.
To summarize the rule of thumb: use .after for scheduled wallpaper, weather and calendar updates; pair .never with an explicit reload from the app for event-driven notification and progress widgets; and use .atEnd only when you can pin entries far into the future.
Entry density and heavy images
Alongside the budget, the other thing that bites is the rendering cost per entry. Wallpaper widgets handle images, and if an entry carries a large image as-is, you hit the memory ceiling and the widget goes blank.
The countermeasure I took was to have entries hold only the image's identifier, not the image itself, and to downsample to the display size in the view before drawing. Handing a full-size image to a widget easily exceeds the tens-of-megabytes memory limit the system imposes.
func downsampledImage(named name: String, to pointSize: CGSize, scale: CGFloat) -> UIImage? { guard let url = imageURL(for: name) else { return nil } let maxPixel = max(pointSize.width, pointSize.height) * scale let options: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxPixel, ] guard let src = CGImageSourceCreateWithURL(url as CFURL, nil), let thumb = CGImageSourceCreateThumbnailAtIndex(src, 0, options as CFDictionary) else { return nil } return UIImage(cgImage: thumb)}
Because a widget's size is limited, the pixels it actually needs are far fewer than the main app's. Just narrowing to the real dimensions with kCGImageSourceThumbnailMaxPixelSize before drawing nearly eliminated the blank widgets.
Signaling priority with relevance
When you stack several widgets in a Smart Stack, TimelineEntryRelevance lets you tell the system "this entry at this time matters." An entry with a higher score is more likely to surface in the stack's automatic rotation.
For a wallpaper app, giving a high score only to the entry at the time I present "today's special photo" raised the odds it actually met the user's eye. Rather than keeping it high all the time, adding contrast only at the moment you want to show something is what works.
Two symptoms I hit as an indie developer
The first is the "freezes in the evening" from the opening. The cause was budget exhaustion from combining .atEnd with short entries. Returning batched entries and switching to .after fixed it.
The second is "I changed the wallpaper in the app but the widget stays old." This came from not notifying a reload from the app side, and it was fixed by calling WidgetCenter.shared.reloadTimelines(ofKind: "WallpaperWidget") right after saving settings. A widget does not pick up the app's state changes automatically, so the side that made the change has to tell it explicitly.
How I decide the design
My conclusion is to first decide "is this widget scheduled or event-driven," and pick .after or .never accordingly. Return entries batched whenever possible, hold images by identifier and shrink them at draw time. Hold to these three and silence from a drained budget largely stops happening.
A widget is not a flashy feature, but because it meets the eye on the home screen every day, a frozen one hurts the impression badly. When you add a widget to a Swift app generated by Rork Max, the same design judgment applies directly. I hope this helps anyone who has puzzled over the same "stops in the evening."
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.