◉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)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●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)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●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-03Advanced

Putting a Wallpaper Shuffle in Control Center — a ControlWidget in a Rork (Expo) App, Rebuilt for iOS 26

Field notes on bolting a Control Center control (ControlWidget) onto a Rork-built React Native wallpaper app — App Group sharing, AppIntent, the Expo config plugin that survives prebuild, and what changed when I rebuilt it for iOS 26 accented rendering.

iOS 265ControlWidgetApp Intents6Expo219wallpaper app22

✦ Premium Article

"Let me change today's wallpaper without unlocking the phone." That request started trickling into the reviews of one of my wallpaper apps (Beautiful Wallpapers) right after iOS 18 shipped. The trouble with wallpaper and calm-utility apps is that opening the app is a surprisingly heavy step, and on the days nobody opens it, no ads are served either.

iOS 18 finally let third-party apps place a control in Control Center, so a single button pulled down from the corner can run logic without launching the app. For a wallpaper app, that is one of the few legitimate ways to claw back the "never opened today" days.

I retrofitted a ControlWidget onto a React Native (Expo) app generated with Rork back then, and recently rebuilt it for the iOS 26 generation. These are the original notes with my current judgment layered on top.

Why a ControlWidget fits a wallpaper app

Custom controls are built as part of WidgetKit. They are cousins of home-screen widgets: you create a small extension target conforming to the ControlWidget protocol, and users can then add your control through Settings or by customizing Control Center.

There are two shapes — ControlWidgetButton and ControlWidgetToggle. Wallpaper shuffle wants the button: tapping it fires an AppIntent that picks the next wallpaper and writes the index to UserDefaults. Because AdMob revenue tracks impressions, adding a lighter touch point in front of the heavy "launch the app" path matters. Even a few percent more sessions per month adds up at the thin per-unit economics of a utility app.

One honest caveat: the control cannot actually set the wallpaper. iOS does not let third-party apps change the lock or home screen wallpaper directly. So I split the design cleanly: the control decides "today's next image," and the user finishes the actual set through a Shortcut or the share sheet. The control owns the decision, not the act.

Architecture — the App Group is the spine

The body of a Rork (Expo) app runs in the JavaScript layer, but a ControlWidget runs as a separate native extension process. They cannot share memory directly, so UserDefaults backed by an App Group becomes the spine.

Three pieces:

  1. The main app (Expo / React Native) writes the wallpaper index into the App Group UserDefaults.
  2. The control extension (Swift / WidgetKit) holds the ControlWidget and AppIntent.
  3. The App Group (an identifier like group.design.dolice.wallpaper) is the shared store.

Built in that order, the data flow stays one-directional: JS writes "total count" and "current position," and the AppIntent reads them to advance the index.

Pull the identifiers into constants. That was the very first thing I did during the rebuild.

enum WallpaperControlKind {
    static let appGroup = "group.design.dolice.wallpaper"
    static let shuffle = "design.dolice.wallpaper.shuffle"
    static let dailyShuffle = "design.dolice.wallpaper.dailyShuffle"
}
✦

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 working ControlWidget + AppIntent that shuffles the wallpaper from Control Center in one tap, with the kind constant factored out
✦The three-part recipe for stateful controls — ControlWidgetToggle, ControlValueProvider, SetValueIntent — and a table for choosing between them
✦An App Group spine for sharing state between the Rork (Expo) JS layer and the native extension, plus a config plugin that survives every prebuild
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-23
Integrating Siri and Shortcuts into Rork Apps: App Intents with Expo, Without the Pitfalls
A production-grade guide to adding Siri Shortcuts and App Intents to a React Native app generated by Rork. Bridge native Swift via an Expo Config Plugin, model AppShortcut / AppEntity / EntityQuery correctly, and ship Donations and Control Center widgets without breaking Apple's conventions.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-10-13
Before You Ship: Go Looking for Your Own Keys Inside the Exported Expo Bundle
A key can be missing from your source and still be sitting inside the finished app. I ran expo export on Expo SDK 57, searched the bundle with grep and a short Node script, and recorded which secrets show up and which don't.