●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
Actually Delivering 'It Updates Without Opening' in Expo — A Realistic Background Task Design
Building 'content refreshes every morning' into a Rork-generated Expo app runs into iOS background execution being far less dutiful than you expect. Here is a minimal expo-background-task setup plus a design that doesn't break when the task never runs.
Running a wallpaper app as an indie developer, the single most requested feature was "I want a new wallpaper every morning without opening the app." Naively, you would just run something periodically in the background and swap the image. But build this seriously on iOS and you end up chasing irreproducible bug reports: "it updated yesterday but not today."
The cause is not a bug in your code — iOS background execution is designed around the premise that you cannot know when it will run. Rork generates an Expo app, but underneath it iOS BGTaskScheduler is at work, and if you build without understanding its capriciousness, your UX is left to chance. This article covers building a refresh experience that survives the task not running, on both the wiring and the judgment side.
Why "It Should Update Every Morning" Doesn't
iOS background tasks are not something you can pin to a clock and force to run. The system runs them only when it decides, based on the device's usage patterns, battery, and network, that "now is acceptable."
So they run fairly often for users who open the app daily, and rarely for users who almost never open it. Ironically, the dormant users who most want "updates without opening" are exactly the ones whose background tasks rarely fire. Accept this asymmetry first or you will design wrong.
A Minimal expo-background-task Setup
Current Expo uses expo-background-task (which uses BGTaskScheduler / WorkManager internally), not the deprecated expo-background-fetch. The task itself is defined with expo-task-manager.
import * as TaskManager from "expo-task-manager";import * as BackgroundTask from "expo-background-task";const REFRESH_TASK = "daily-wallpaper-refresh";TaskManager.defineTask(REFRESH_TASK, async () => { try { const updated = await fetchAndCacheTodaysWallpaper(); // Always return success/failure. Not returning makes iOS shrink your future budget return updated ? BackgroundTask.BackgroundTaskResult.Success : BackgroundTask.BackgroundTaskResult.Failed; } catch { return BackgroundTask.BackgroundTaskResult.Failed; }});export async function registerRefreshTask() { const status = await BackgroundTask.getStatusAsync(); if (status !== BackgroundTask.BackgroundTaskStatus.Available) return; await BackgroundTask.registerTaskAsync(REFRESH_TASK, { minimumInterval: 60 * 12, // minutes; 12 hours here });}
In app.json, iOS UIBackgroundModes must include processing. In Expo you write it under infoPlist.
Forget to list the task ID in BGTaskSchedulerPermittedIdentifiers and registration succeeds but the task never runs on device — a silent failure. I lost half a day to exactly this.
✦
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
✦Get a working minimal setup of expo-background-task and expo-task-manager, including the UIBackgroundModes configuration
✦Understand that minimumInterval is a floor, not a guarantee, expressed through concrete iOS conditions: Low Power Mode, launch frequency, charging state
✦Implement a two-tier foreground fallback that keeps a wallpaper app's auto-refresh from breaking when background never fires
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.
Setting minimumInterval to 12 hours only specifies a lower bound: "do not run more often than every 12 hours." There is no upper bound; if conditions are not met it may not run for 24 or 48 hours.
Read it as "runs every 12 hours" and you will feel reassured in testing — where it happens to fire — only to get bitten by irreproducible reports on real user devices. You are expressing a wish; the OS holds the decision.
The Conditions Under Which iOS Skimps
Three typical conditions lower the run probability: Low Power Mode is on, the app is launched infrequently, and the device goes long stretches without charging or network.
Conversely, a device charging overnight on Wi-Fi has the conditions aligned for background execution. The reason a wallpaper app can plausibly deliver "updated by morning" is precisely that it can lean on the overnight charging window. When designing, picture concretely which time window and which device states you are betting on, and your expectations land closer to reality.
A Two-Tier Design That Survives Failure
The conclusion: treat background refresh as a lucky bonus, and make the real workhorse a foreground fallback on launch.
// Every time the app returns to foreground, check last refresh and update if staleimport { AppState } from "react-native";AppState.addEventListener("change", async (next) => { if (next !== "active") return; const last = await getLastRefreshedAt(); const stale = Date.now() - last > 1000 * 60 * 60 * 12; if (stale) await fetchAndCacheTodaysWallpaper();});
With this two-tier approach, users whose background ran are already updated before opening, and users whose background did not run get updated the moment they open. Neither path leaves them stale. After switching to this design, refresh-related inquiries to my App Store support nearly stopped.
Testing on a Real Device
The simulator does not faithfully reproduce background behavior. Test on a real device and trigger the task manually from the Xcode debugger. During a debug session, this LLDB command fires a registered task immediately.
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"daily-wallpaper-refresh"]
After confirming the work runs correctly via this manual trigger, log "how many hours later did it actually run on its own" over a few days, and your app's realistic run frequency comes into view.
Grasp the Run Frequency in Logs Before Deciding
Talking from intuition leads to design mistakes, so I recommend first grasping your app's actual run frequency in numbers. In my case, I logged the execution time to a server every time BackgroundTaskResult returned, and aggregated a week's worth.
Capture Date.now() at the top of the task body and send it with the result code.
Compute "elapsed time since last run" per device.
Average the share that ran within 24 hours across all active devices.
What this revealed was a clear gap: for users who open the app daily, the within-24-hour run rate reached roughly 70%, while for dormant users who open it once a week it fell below 20%. Background refresh runs more for high-retention users and barely reaches low-retention ones — the asymmetry, confirmed in numbers.
The production lesson is that shortening minimumInterval to lift this rate barely helps, because the OS decides on device usage, not your interval hint. Shortening it can even backfire, burning the budget before the system decides "now is fine." I keep the interval at a practical floor (12 hours for me) and lift frequency with the foreground fallback — that division of roles is where I settled.
How Far to Lean on Background, and What to Give Up
Background execution is excellent as a supplement that lifts the baseline UX, but fragile as a feature precondition. "Updates without opening" is best designed as a delight when it lands, not a promise — that is my settled compromise for running apps long-term as a solo developer.
App Store review can also flag leaving update certainty (like reflecting purchase state) to the background. Push certainty-critical work to the foreground or the server — decide that line up front and you avoid rebuilding the design later.
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.