◉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, 27 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, 27 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/Dev Tools
⬡ Dev Tools/2026-06-17Advanced

Why Your Rork Max Native Swift Widget Freezes After Day One — Designing the TimelineProvider Refresh Budget

Native Swift home screen widgets generated by Rork Max stop rotating after the first day unless you understand the TimelineProvider refresh budget. Here is how reloadPolicy, App Groups, and deep links fit together in a real app.

Rork Max235WidgetKit11Swift48Home Screen WidgetIndie Development31

✦ Premium Article

If you run wallpaper apps or affirmation apps as an indie developer for any length of time, the request "show one fresh image on the home screen each day" comes up sooner or later. Once Rork Max started emitting native Swift widgets, I retrofitted WidgetKit onto one of my existing apps — and the very first build tripped me up. It rotated daily in the simulator, but when I installed it on a real device and checked the next morning, the widget was frozen on yesterday's single image.

The cause was that the generated TimelineProvider never decided when or how much refresh to schedule. A WidgetKit timeline is not a cron. Ship Rork Max's output without designing this part, and you get the nastiest failure mode of all: it works on day one and dies on day two.

Widgets freeze because they exhaust the budget

To protect battery life, WidgetKit caps how many refreshes each widget can receive per day — a refresh budget. Apple's public guidance puts this at roughly 40 to 70 refreshes per day for a frequently viewed home screen widget, and both consuming the entries your TimelineProvider returns and your calls to reloadTimelines(ofKind:) draw down that same budget.

The key detail: regeneration of the next timeline does not happen automatically once the last entry passes. If getTimeline returns "just one entry for midnight today" and you specify no reloadPolicy, the system has no idea when to load the next one. The display stays pinned to that final entry. The reason you miss it in the simulator is that the timeline is regenerated every time you run a debug build — that is a different world from production behavior.

Because the budget does not recover until the next day once spent, the design has two goals. First, keep the daily refresh count inside the budget. Second, reliably hand off to the next day within that limit. For a daily rotation, the truly necessary refresh is once per day. Rather than slashing refreshes out of budget anxiety, it is safer to use reloadPolicy correctly to state exactly when the next refresh should happen.

Always declare "when to wake up next" with reloadPolicy

The policy argument of Timeline(entries:policy:) has three forms. .atEnd reloads after the last entry passes, .after(date) reloads at a specified time, and .never means no reload. For a daily widget, naming midnight tomorrow with .after is the most predictable.

import WidgetKit
import SwiftUI
 
struct DailyEntry: TimelineEntry {
    let date: Date
    let title: String
    let imageName: String
}
 
struct DailyProvider: TimelineProvider {
    func placeholder(in context: Context) -> DailyEntry {
        DailyEntry(date: Date(), title: "Today's Pick", imageName: "placeholder")
    }
 
    func getSnapshot(in context: Context, completion: @escaping (DailyEntry) -> Void) {
        completion(currentEntry())
    }
 
    func getTimeline(in context: Context, completion: @escaping (Timeline<DailyEntry>) -> Void) {
        let entry = currentEntry()
 
        // Name midnight tomorrow (device local) as the next refresh point
        let calendar = Calendar.current
        let startOfTomorrow = calendar.startOfDay(
            for: calendar.date(byAdding: .day, value: 1, to: entry.date)!
        )
 
        let timeline = Timeline(entries: [entry], policy: .after(startOfTomorrow))
        completion(timeline)
    }
}

The crux here is narrowing the entries to "one for today" and setting reloadPolicy to .after(midnight tomorrow). The system then calls getTimeline again around midnight, where "tomorrow's image" gets resolved. The effective refresh is once a day, which does not strain the budget.

.atEnd works too, but with only one entry queued, "the last entry" equals "what is showing now," and the reload timing becomes ambiguous. If you want a clean switch at the date boundary, holding the time with .after behaves as intended. My own first build used .atEnd, froze the next day, and was fixed by switching to .after — so for daily use I now default to .after.

✦

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
✦A daily-rotating widget design that keeps refreshing past day one by working with reloadPolicy and the system refresh budget
✦An App Group pattern that lets the main app and the widget share the same content so today's entry resolves without networking
✦Deep-link design that routes a widget tap to the correct screen via a Widget URL, with the common failures isolated
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.
⬡ Dev Tools2026-10-27
Claude Sonnet 4.5 Leaves the API on November 30. Find the Model IDs Hard-Coded in Your App First
Claude Sonnet 4.5 is scheduled to retire from the API on November 30, 2026. A small script finds the model IDs written into your app's code and config, so you can replace them and check again.
⬡ Dev Tools2026-09-05
Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler
Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.