●MAX — Rork Max bills itself as the first web Swift app builder, publishing to the App Store in two clicks with no Xcode required●APPLE — It generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro●EXPO — The standard tier builds native iOS and Android apps on React Native (Expo) from a plain-English description●FUNDING — Rork raised $2.8M from a16z, strengthening its position in AI no-code mobile development●PRICE — Free to start, with paid plans from $25/month — an accessible entry point for solo developers●WWDC — WWDC 2026 pushes Apple Intelligence forward, raising the value of native features and widening AI integration options for no-code apps●MAX — Rork Max bills itself as the first web Swift app builder, publishing to the App Store in two clicks with no Xcode required●APPLE — It generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro●EXPO — The standard tier builds native iOS and Android apps on React Native (Expo) from a plain-English description●FUNDING — Rork raised $2.8M from a16z, strengthening its position in AI no-code mobile development●PRICE — Free to start, with paid plans from $25/month — an accessible entry point for solo developers●WWDC — WWDC 2026 pushes Apple Intelligence forward, raising the value of native features and widening AI integration options for no-code apps
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.