●BUILD — Rork Max generates native Swift apps, reaching areas React Native struggles to touch●PLATFORM — Rork Max supports iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Tap native features like HealthKit, Core ML, NFC, Dynamic Island, and Live Activities●TEST — A browser-based streaming iOS simulator lets you test without Xcode or a Mac●DEPLOY — Automated builds, certificates, and App Store submission simplify shipping●PRICE — Start free; paid plans begin at $25/month and Rork Max is $200/month●BUILD — Rork Max generates native Swift apps, reaching areas React Native struggles to touch●PLATFORM — Rork Max supports iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Tap native features like HealthKit, Core ML, NFC, Dynamic Island, and Live Activities●TEST — A browser-based streaming iOS simulator lets you test without Xcode or a Mac●DEPLOY — Automated builds, certificates, and App Store submission simplify shipping●PRICE — Start free; paid plans begin at $25/month and Rork Max is $200/month
Putting Your Own Control in Control Center — ControlWidget Design That Adds Launch Paths to a Rork Max App
Implementing iOS 18 ControlWidget to place custom controls in Control Center, the Lock Screen, and the Action button: working ControlWidgetButton and ControlWidgetToggle code, its value as a launch path, and how it feeds retention.
A few years ago I added a Home Screen widget to one of my live apps. The feature set barely changed, yet morning-hour launches rose visibly. Ever since, whenever the OS grows a new "place to put things," I evaluate adding a launch path before I evaluate adding a feature.
iOS 18's new place is Control Center. With WidgetKit's ControlWidget, you can put your own buttons and toggles into Control Center, the Lock Screen, and the Action button. Rork Max handles native Swift and extension targets, so this territory is reachable — as long as you know how to phrase the instructions.
As a feature it is unglamorous. But as a device for getting an indie app remembered daily, its return on effort is among the better ones I know.
Where It Sits — Compared to the Places You Already Use
If you already ship Home Screen widgets or App Shortcuts, here is how the three differ in character.
Aspect
Home Screen widget
App Shortcuts (Siri/Spotlight)
ControlWidget
User effort
Navigate to the Home Screen and look
Search or speak
One swipe from any screen
Expressiveness
High (information display)
Low (text-centric)
Low (an icon and one action)
Best for
Ambient state display
Operations you call by name
Reflexive one-shot actions
The essence of ControlWidget is that it is one swipe away from any screen. It trades expressiveness for minimal reach cost. Creating a new note, starting or stopping a recording, opening today's single item — pick exactly one operation that fingers perform without thinking, and put that there.
In retention terms, this parks a launch trigger on prime OS real estate. If your app runs on ad revenue, lifting DAU lifts revenue directly. Having run AdMob-supported apps for years as an indie developer, I find investments in "remembering devices" like this far easier to forecast than feature work.
The Minimal Implementation — a Button That Opens the App
ControlWidgets are defined inside a widget extension target. Start with the simplest form: a button that opens a specific screen.
import WidgetKitimport SwiftUIimport AppIntents// The intent fired by the control; openAppWhenRun brings the app to the foregroundstruct OpenQuickMemoIntent: AppIntent { static let title: LocalizedStringResource = "Open Quick Memo" static let openAppWhenRun: Bool = true func perform() async throws -> some IntentResult & OpensIntent { // The app receives this via onOpenURL or scene restoration return .result(opensIntent: OpenURLIntent(URL(string: "myapp://quick-memo")!)) }}struct QuickMemoControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "com.example.myapp.quickmemo") { ControlWidgetButton(action: OpenQuickMemoIntent()) { Label("Quick Memo", systemImage: "square.and.pencil") } } .displayName("Quick Memo") .description("Opens memo entry from any screen.") }}
Two things matter here. Forget openAppWhenRun = true and the intent executes in the background without ever surfacing your app. And the deep link target (myapp://quick-memo in this example) needs a separate receiving implementation in the app itself. The control is the front door; guiding the visitor is the app's job.
✦
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
✦Working code for ControlWidgetButton, ControlWidgetToggle, and ControlValueProvider, plus how to split Rork Max prompts around the widget extension target
✦Where ControlWidget sits next to Home Screen widgets and App Shortcuts as a launch path — and how it feeds retention and, by extension, ad revenue
✦Fixes for the traps everyone hits once: controls that never update, controls missing from the gallery, and taps that do nothing
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.
Stateful Features Get a Toggle — ControlWidgetToggle
For binary features like start/stop recording, use ControlWidgetToggle, and implement a ControlValueProvider to supply the current state.
struct RecordingControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.example.myapp.recording", provider: RecordingStateProvider() ) { isRecording in ControlWidgetToggle( "Recording", isOn: isRecording, action: ToggleRecordingIntent() ) { on in Label(on ? "Recording" : "Stopped", systemImage: on ? "record.circle.fill" : "record.circle") } } .displayName("Start/Stop Recording") }}struct RecordingStateProvider: ControlValueProvider { // The value used for the gallery preview var previewValue: Bool { false } func currentValue() async throws -> Bool { // Share state with the main app through an App Group UserDefaults(suiteName: "group.com.example.myapp")? .bool(forKey: "isRecording") ?? false }}
Toggle state must be shared with the main app, so use App Group UserDefaults or a shared file. And whenever the app changes that state, call ControlCenter.shared.reloadControls(ofKind:) to refresh the control's display. Skip this and you get the classic inconsistency: recording stopped in the app while the control still says otherwise. Everyone hits this once — make "state change plus reload" a single habit and you never hit it twice.
Prompting Rork Max — Split Around the Extension Target
Because controls live in the widget extension, split your Rork Max instructions along that boundary. My three-pass split:
"Add an iOS 18 ControlWidget to the widget extension. Use a reverse-domain kind; assume it can be placed in Control Center, the Lock Screen, and the Action button."
"Make the AppIntent use openAppWhenRun = true and open myapp://quick-memo."
"Add a myapp:// scheme receiver in the main app that routes quick-memo to the memo entry screen."
Features spanning the extension and the main app tend to come out half-finished when packed into one prompt. Working in the order extension, then intent, then app-side receiver — verifying each pass — keeps the generated code checkable.
If your deployment target requires if #available(iOS 18.0, *) branching, say so explicitly. Without it, some target configurations simply fail to build.
The Job Is Not Done Until It Gets Placed — In-App Guidance
The hard part of ControlWidget is not the code. Users must add controls themselves from the Control Center editing gallery, and most never discover that this exists.
My recommendation: show a single in-app prompt, once, only to users who have used the underlying feature at least three times. One sheet saying the feature is also reachable from Control Center, with the three-step add instructions. Show it to everyone on first launch and it is noise; show it to no one and the control may as well not exist. Targeting engaged users is the calibration that has felt right in my own apps.
Instrument it, too. Send an analytics event inside the intent's perform() and you can track what share of launches arrive via the control. Investment decisions about launch paths only become possible with that number.
A Short List of Traps
Finally, the places where hands stop during implementation.
Not appearing in the gallery: the extension build may be stale. Relaunching the app on the device usually refreshes it
Tap does nothing: openAppWhenRun is missing, or no URL scheme receiver exists. Isolate by invoking the intent alone from App Shortcuts first
State never updates: a missing reloadControls(ofKind:). Grep every location that writes the shared state and check each one
It is an unshowy API, but as scaffolding for becoming a daily-use app, it is dependable. Start with a single button that just opens the app, and see where it takes you.
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.