●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
Protecting Ad eCPM in Your Rork Max App: Designing ATT Pre-Permission Priming
For iOS apps built with Rork Max, ad revenue swings heavily on your ATT opt-in rate. Here is how to design a pre-permission priming screen, implement it in SwiftUI, measure the opt-in rate, and order AdMob init correctly.
When ad revenue on iOS comes in lower than expected, the cause is usually not your ad network or your bid floors — it is your ATT (App Tracking Transparency) opt-in rate. As an indie developer running wallpaper apps, I have watched the same ad SDK in the same placement produce noticeably different eCPM as the opt-in rate moved by a dozen points.
Because Rork Max generates native Swift, you get finer control over ATT than the React Native path gives you. Whether you treat that control as a design problem is what decides your post-launch revenue.
The Real Bottleneck Is Consent, Not Bidding
Ad revenue is roughly impressions times eCPM. Most solo developers obsess over impressions — retention, session frequency — but the biggest lever on the eCPM side is your ATT opt-in rate.
When a user taps "Allow," your app can read the IDFA and networks can bid with personalization. When they tap "Ask App Not to Track," the IDFA returns as zeros and bidding falls back to contextual signals only. That gap shows up directly as a price gap.
The key insight is that the opt-in rate is half-independent of how good your app is. In other words, you can move it just by designing how you ask. I decide this as a revenue design step before writing any code.
How IDFA Drives eCPM, in Numbers
Why does allow-versus-deny create such a spread? Personalized ads let networks serve higher-value inventory based on user interest. Without an IDFA, advertisers are unsure who they are reaching, so they bid conservatively.
Here is a rough directional picture close to what I see in production (it varies by app, region, and format, so treat it as direction, not a promise).
ATT status
IDFA
eCPM trend
Revenue impact
Authorized
Available
Baseline (100%)
Fully monetizable
Denied
Zeroed
Around 40–60%
Price drops sharply
Not Determined
Unavailable
Same as denied
Missed prompt, missed revenue
The "Not Determined" row is the one people miss. If you never show the prompt, or you show it at the wrong moment and get an instant rejection, users who would have said yes end up counting as no. That is the first hole to plug.
✦
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
✦Reframe weak ad revenue as an ATT opt-in problem rather than a bidding problem, and start protecting eCPM today
✦Get working SwiftUI code for a pre-permission priming screen in Rork Max, plus the events you need to measure and A/B the opt-in rate
✦Fix the AdMob init ordering that quietly leaks revenue, so your first ad request actually carries an IDFA
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.
The System ATT Dialog Fires Once, So Design the Ask
The system ATT dialog has a constraint that trips up developers: you effectively get one shot. Once a user picks "Ask App Not to Track," you cannot re-present it from inside the app — you can only send them to Settings.
That is why firing the system dialog immediately is a poor move. A permission request that appears with no context gets reflexively rejected. The fix is pre-permission priming.
Think of it as three steps.
Show your own explanation screen (the priming screen) that states, in one line, why you are asking for tracking permission.
Only when the user taps "Continue" do you show the system ATT dialog.
If they choose "Not now" on the priming screen, preserve the system dialog and re-show only the priming later, once the context is right.
With this design, you fire the one-shot system dialog mainly at people likely to allow it.
Implementing the Priming Screen in Rork Max (SwiftUI)
Ask Rork Max to "build a screen that explains the tracking permission" and you get a starting point, but I prefer to own the ATT state transitions and timing myself. I reorganize the generated code into explicit state management like this.
Priming Screen Requirements
Show it after the user has experienced the app's value once, not right before ad SDK init
Keep the copy honest: "so we can keep the app free with relevant ads"
Offer two choices, "Continue" and "Not now," and keep the app fully usable if they decline
The Code
import SwiftUIimport AppTrackingTransparencyimport AdSupport// A wrapper that makes ATT state easy to handle from SwiftUI@MainActorfinal class TrackingCoordinator: ObservableObject { @Published var showPriming = false // Call once on launch. Only queue priming when status is undetermined. func evaluateOnLaunch() { let status = ATTrackingManager.trackingAuthorizationStatus if status == .notDetermined { // Do not show yet. Set showPriming after a value moment. showPriming = false } } // Call when the user taps "Continue" on the priming screen. func requestSystemPrompt() async -> Bool { let status = await ATTrackingManager.requestTrackingAuthorization() let authorized = (status == .authorized) Analytics.log(event: "att_result", params: ["authorized": authorized]) return authorized }}
The priming screen itself is a plain SwiftUI sheet.
struct TrackingPrimingView: View { @EnvironmentObject var coordinator: TrackingCoordinator var onFinish: (Bool) -> Void var body: some View { VStack(spacing: 20) { Image(systemName: "hand.raised.circle") .font(.system(size: 56)) Text("Ads that fit you better") .font(.headline) Text("To keep this app free, we show relevant ads. Choosing Allow on the next screen means more useful ads for you.") .font(.callout) .multilineTextAlignment(.center) .padding(.horizontal) Button("Continue") { Analytics.log(event: "att_priming_continue") Task { let ok = await coordinator.requestSystemPrompt() onFinish(ok) } } .buttonStyle(.borderedProminent) Button("Not now") { Analytics.log(event: "att_priming_skip") onFinish(false) } .font(.footnote) } .padding() }}
When to Call the ATT Request
requestTrackingAuthorization must be called while the app is active in the foreground. If you call it during the splash screen at launch, the dialog can fail to appear and simply return .denied. I use "after the first main screen renders and the user takes one or two actions" as the trigger.
Measure the Opt-In Rate to Improve Copy and Timing
Priming is worth nothing until you measure it. At minimum, capture these three events and you will see where people drop off.
Priming screen shown (att_priming_shown)
"Continue" tapped (att_priming_continue)
System dialog result (att_result carrying the authorized flag)
From those three points, compute two rates: the "Continue" pass rate on the priming screen, and the final authorization rate on the system dialog.
enum Analytics { // In a real app, swap this for Firebase Analytics or similar. static func log(event: String, params: [String: Any] = [:]) { // e.g. later aggregate priming-shown rate and final allow rate AnalyticsBridge.shared.record(name: event, parameters: params) }}
Once you measure, you will see a single word of copy or a shift in timing move the pass rate. In my wallpaper apps, simply showing the prompt "after a value moment" rather than at launch moved my final allow rate by more than ten percent in feel. Always re-measure on your own app.
Do Not Get the AdMob Init Order Wrong
This is the trap most easily missed in production. If you initialize AdMob (the Google Mobile Ads SDK) before the ATT result is settled, your first ad request goes out with no IDFA and you lose that revenue outright.
The correct order is "consent (UMP if needed) → ATT result settled → AdMob init → first ad request." That serial section must not be parallelized. I collapse startup into a single bootstrap function that makes ad init wait until ATT resolves.
func bootstrapMonetization(coordinator: TrackingCoordinator) async { // 1) Settle consent (UMP) first if required await ConsentManager.shared.gatherIfNeeded() // 2) Wait for the ATT result (decided via the priming flow) _ = await coordinator.requestSystemPrompt() // 3) Only now initialize the ad SDK await AdNetwork.shared.start() // 4) Preload the first ad after init completes AdNetwork.shared.preloadInterstitial()}
One caution: do not call requestTrackingAuthorization twice. If it fires once via priming and once via bootstrap, the state races and debugging gets painful. Keep the call site in one place.
There is one more production caution worth stating. If you serve the EU, the UMP (consent management) form and ATT are separate things, and both must pass in the right order. I lock the sequence to "settle UMP first, then show ATT priming." Reverse it and, in some regions, ad init can proceed while consent is still unresolved, which can halt delivery entirely. Always test on a real device from a reset state (set tracking back to undetermined in Settings, or do a fresh install rather than using the simulator). ATT behavior in the simulator does not always match a device, and that mismatch is an easy place to lose an afternoon.
Common Mistakes and the Calls I Make
Firing the system dialog at launch → easily rejected with no context. I always insert priming.
Never showing it again after a decline → leave room to re-show only the priming later, after another value moment (the system dialog is still preserved).
Running ad init at the top of application(_:didFinishLaunchingWithOptions:) → it tends to run before ATT, so move it into bootstrap.
Not measuring the opt-in rate → improvements become guesswork. I recommend adding at least the three events above.
For App Store review, you must include NSUserTrackingUsageDescription (the Info.plist purpose string) to present ATT. Rork Max output sometimes drops it, so verify before you submit.
Your Next Step
Start by wiring just two events, att_priming_shown and att_result, and measure your current final allow rate. Once you can see the number, whether to fix the copy or the timing becomes a concrete decision. I usually treat this as the starting point for revenue work — getting the "opt-in rate" foundation right tends to be the shorter path than chasing raw ad prices.
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.