●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing
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.
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 WidgetKitimport SwiftUIstruct 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.
Use App Groups to tie the app and widget to one truth
A widget runs in a separate process from the main app, so it cannot directly read UserDefaults.standard or files inside the app's sandbox. To let the main app decide "which image is today's" and have the widget read it, you go through an App Group shared container.
Rork Max output sometimes ships without the App Group entitlement, and I have seen this leave a widget stuck on its placeholder forever. There are three steps:
Add the same App Group (for example group.net.dolice.example) to Signing & Capabilities for both the main app and the widget extension
Write today's selection from the main app into the shared UserDefaults
Read it back from the same App Group inside the widget's getTimeline
// Main app: resolve today's pick and write it to the shared containerenum DailyStore { static let suiteName = "group.net.dolice.example" static func writeToday(title: String, imageName: String) { let defaults = UserDefaults(suiteName: suiteName) defaults?.set(title, forKey: "today.title") defaults?.set(imageName, forKey: "today.image") defaults?.set(Date(), forKey: "today.savedAt") }}// Widget: read from the shared container (fall back if absent)func currentEntry() -> DailyEntry { let defaults = UserDefaults(suiteName: DailyStore.suiteName) let title = defaults?.string(forKey: "today.title") ?? "Today's Pick" let image = defaults?.string(forKey: "today.image") ?? "placeholder" return DailyEntry(date: Date(), title: title, imageName: image)}
The benefit of this design is that the widget never has to hit the network. The main app resolves today's content first — at launch or on a date-change notification — and the widget only reads the shared container. Running a heavy download inside the widget's getTimeline strains both the refresh budget and the execution time, which tends to delay or drop the display. The split of "main app resolves, widget only reads" stays stable.
When the main app rewrites today's pick, prompt the widget to reload explicitly.
import WidgetKit// Call this after updating today's pick in the main appDailyStore.writeToday(title: newTitle, imageName: newImage)WidgetCenter.shared.reloadTimelines(ofKind: "DailyWallpaperWidget")
reloadTimelines(ofKind:) also consumes the refresh budget, so it matters not to spam it on every user action. For a daily rotation, limiting it to "when the date changes" and "when the user manually changes today's pick" stays comfortably within budget.
Route the widget tap to the right screen
A daily widget should jump to the matching wallpaper detail or affirmation screen when tapped. Since systemSmall is a single tap target across its whole area, use widgetURL; when you want per-element links on systemMedium and larger, use Link.
struct DailyWidgetEntryView: View { var entry: DailyEntry var body: some View { VStack { Image(entry.imageName).resizable().scaledToFill() Text(entry.title).font(.caption) } // For the small size, assign one URL to the whole widget .widgetURL(URL(string: "dolice-example://daily?image=\(entry.imageName)")) }}
The receiving side interprets the scheme via SwiftUI's onOpenURL or application(_:open:options:). Two failures are common here. One is forgetting to register the URL scheme in Info.plist, so a tap merely opens the app without navigating. The other is mixing widgetURL and Link in the same view hierarchy, so on systemSmall the Link side is ignored and you land somewhere unintended. Deciding to use only one per size makes the diagnosis far easier.
A procedure to confirm it "still works tomorrow" on device
The simulator regenerates the timeline every run, so you can only hit this class of bug on a real device. Here is the check I always run before shipping a daily widget:
Install on a device and place the widget on the home screen at systemSmall
Manually advance the device date to tomorrow, wait a few minutes, and watch whether the widget switches
Reboot the device and confirm the reloadPolicy reservation is honored after restart
Call WidgetCenter.shared.reloadTimelines(ofKind:) from the main app and confirm an immediate update
Re-run steps 2 and 4 in airplane mode to confirm today's entry appears without networking
Step 5 in particular is effective at exposing whether the widget accidentally became network-dependent. If today's entry is written to the App Group, it displays fine even in airplane mode. If it does not, the widget is most likely doing networking inside getTimeline that it should never do.
How much to delegate to Rork Max, and where to take control
Rork Max's native Swift output is genuinely helpful for standing up the widget views and the basic structure in one shot. The invisible operational nuances, though — the choice of reloadPolicy, the App Group entitlement, the assumptions behind the refresh budget — surface as "works on day one, broken on day two" if you trust the generated output blindly.
My own call is a practical line: let Rork Max own the views and wiring, and take control of the reloadPolicy in getTimeline and the shared-storage read/write myself. Even a small feature like one image a day demands a design decision once the refresh budget is in play. Start by placing one systemSmall widget on your own app and advancing the device date to tomorrow to see whether it switches. Clear that, and you have the foundation to extend toward multiple sizes and per-element links.
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.