●BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the output●NATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other builders●PLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screen●COMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to end●PRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that back●DEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify●BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the output●NATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other builders●PLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screen●COMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to end●PRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that back●DEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
Keep Your Rork App's Review From Stalling on a Privacy Manifest Gap
Handle PrivacyInfo.xcprivacy and Required Reason APIs in a Rork Expo app — a common cause of App Store review stalls — covering app.config.ts setup, collected-data declarations, third-party SDK checks, and a pre-submission verification script.
You submit the app, and just when you think it cleared, an email arrives from Apple: "your app uses an API without a declared reason" — that warning that starts with ITMS. As an indie developer shipping several apps to the App Store and Google Play, the first time I got that notice I froze, with no idea what to fix.
Rork emits an Expo (React Native) app, and inside it many native APIs and third-party SDKs are running. Since 2024 Apple requires you to declare, in a privacy manifest, the "reason" for using certain APIs. Without the declaration it is a warning email today, but it will be upgraded to a rejection. Here, I work through this for a Rork project from both sides — your own code and the SDKs.
What is actually being asked
Apple requires two broad things.
Shipping a "privacy manifest" (PrivacyInfo.xcprivacy) that declares the kinds of data your app collects and how they are used.
Declaring, with a prescribed code, why you use certain APIs known as "Required Reason APIs."
Required Reason APIs are a set — file timestamps, free disk space, system boot time, UserDefaults — that can be abused for fingerprinting. If you use them for legitimate purposes, you pass simply by declaring the right reason code. The catch is that in most cases you are not calling them directly — a dependency is.
These are two separate requirements. Satisfying one leaves the other's gaps untouched, so the sections below handle them apart.
First, learn to read the warning email
The email lists the missing API categories, for example "no reason declared for NSPrivacyAccessedAPICategoryUserDefaults." Not skimming this is the start of the fix.
The first thing I did was copy the category names from the email and, for each, sort out "is this from my code or from an SDK?" Things like UserDefaults that the app itself uses go on my side; things an ad SDK touches internally go on the SDK side. That sorting instantly clarifies where to fix.
✦
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
✦Use a lookup table of Expo features to API categories so you can write the declarations before the warning email ever arrives
✦Cover both halves of the requirement — Required Reason API declarations and the separate NSPrivacyCollectedDataTypes filing — without dropping either
✦Turn the pre-submission bundling check into a script that returns an exit code, so every release is verified mechanically
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.
You can work out the categories from your features, in advance
If you only start investigating once the warning arrives, you have already added a round trip to your submission. But you can predict what your app trips with surprising accuracy. Rork's generated code sits on top of the Expo module set, so "if you do this, you will be declaring that category" is largely determined up front.
Here is the mapping I put together for my own Rork projects.
What the app does
Category it trips
Applicable reason code
Practical note
Storing settings and flags — not by touching it yourself, but because an Expo module or SDK uses NSUserDefaults internally
UserDefaults
CA92.1
The one you trip without noticing. Nearly every app qualifies
Sharing settings with a widget or share extension via an App Group
UserDefaults
1C8F.1
A different code from app-only storage
Reading a downloaded file's modified time to decide whether to refetch
FileTimestamp
C617.1
The code for files inside your own container only
Showing the date of a photo or document the user picked
FileTimestamp
DDA9.1
Displaying is a different code from deciding internally
Checking free space before a write so it does not fail
DiskSpace
E174.1
Same code when you use it to decide what cache to purge
Showing storage usage on a settings screen
DiskSpace
85F4.1
"Showing" is separate. Do both and you declare both
Measuring elapsed time or a cooldown from boot
SystemBootTime
35F9.1
Often tripped quietly by analytics SDKs
The table assumes "if that is your use." The same category takes a different reason code for a different purpose, and picking a code that does not match reality is itself a false declaration. Rather than copying the table, check once which row your implementation actually lands on.
Write the manifest in app.config.ts
In Expo, rather than editing the native PrivacyInfo.xcprivacy directly, the right path is to write the declaration in the iOS config of app.config.ts (or app.json). Do this and the manifest ships automatically every time you rebuild the Rork project — no manual re-pasting.
// app.config.tsexport default { expo: { ios: { privacyManifests: { NSPrivacyAccessedAPITypes: [ { NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryUserDefaults", // CA92.1 = UserDefaults access for the app itself only NSPrivacyAccessedAPITypeReasons: ["CA92.1"], }, { NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryFileTimestamp", // C617.1 = timestamps of files inside your own container NSPrivacyAccessedAPITypeReasons: ["C617.1"], }, { NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategoryDiskSpace", // E174.1 = checking free space before writing NSPrivacyAccessedAPITypeReasons: ["E174.1"], }, { NSPrivacyAccessedAPIType: "NSPrivacyAccessedAPICategorySystemBootTime", // 35F9.1 = measuring time elapsed between in-app events NSPrivacyAccessedAPITypeReasons: ["35F9.1"], }, ], }, }, },};
Pick reason codes from Apple's defined list that match your use. NSPrivacyAccessedAPITypeReasons is an array because one category can serve several purposes. If you use free-space checks both before a write and to display usage on a settings screen, list E174.1 and 85F4.1 together. Declaring only one while doing both is its own kind of gap.
The collected-data filing is separate from Required Reason APIs
Everything above covers only the second of the two requirements from the opening. The other one — declaring what you collect via NSPrivacyCollectedDataTypes — is still outstanding. Clearing the Required Reason API warnings while leaving this empty is an incomplete filing.
For a Rork app monetized with AdMob, the advertising identifier and crash data are the minimum in scope.
// app.config.ts — inside ios.privacyManifestsprivacyManifests: { NSPrivacyTracking: true, NSPrivacyTrackingDomains: ["googleads.g.doubleclick.net"], NSPrivacyCollectedDataTypes: [ { NSPrivacyCollectedDataType: "NSPrivacyCollectedDataTypeDeviceID", NSPrivacyCollectedDataTypeLinked: false, NSPrivacyCollectedDataTypeTracking: true, NSPrivacyCollectedDataTypePurposes: [ "NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising", ], }, { NSPrivacyCollectedDataType: "NSPrivacyCollectedDataTypeCrashData", NSPrivacyCollectedDataTypeLinked: false, NSPrivacyCollectedDataTypeTracking: false, NSPrivacyCollectedDataTypePurposes: [ "NSPrivacyCollectedDataTypePurposeAppFunctionality", ], }, ], NSPrivacyAccessedAPITypes: [ // the Required Reason API declarations from above go here ],}
Linked means tied to the user's identity; Tracking means combined with other companies' data for tracking. If you hand the advertising identifier to third-party advertising, Tracking is true.
There is one trap here that actually changes runtime behavior. Domains listed in NSPrivacyTrackingDomains are blocked by the OS whenever App Tracking Transparency permission has not been granted. The declaration is not a safety blanket — network behavior changes the moment you write it. Discovering this backwards, from what looks like a drop in ad fill rate, costs real time to isolate. Before listing a domain, confirm your ATT consent flow is implemented and actually appearing.
One more thing. This manifest does not replace your "App Privacy" answers in App Store Connect. Both are required, and a mismatch between them gets flagged in review. The manifest covers what your own code collects; an SDK's share is carried by the SDK's own manifest. Your App Store Connect answers, by contrast, must cover the whole picture including what SDKs collect. That asymmetry is easy to miss, so I have fixed my order of operations: edit app.config.ts, then go straight to App Store Connect and re-read the answers.
Do not skip the third-party SDK check
This is the most overlooked pitfall. However well you set up your own app.config.ts, if AdMob or an analytics SDK does not carry its own privacy manifest, the warning will not clear.
The procedure looks like this.
Bump the SDK versions. Most major SDKs ship the manifest in their compliant releases. AdMob (the Google Mobile Ads SDK) has shipped it since a certain version too.
If an SDK update alone does not cover it, add the API categories that SDK touches to your own manifest and declare the reason.
Confirm that each SDK's .xcprivacy is included inside the built .app.
In my case, the warning persisted because of a leftover old analytics library. Bumping one SDK cleared two warnings at once, so I find it efficient to start from updating dependencies.
Verify it yourself before submitting
Waiting for the warning after submitting wastes time. After exporting the archive, I confirm the bundled state locally before submitting. I look inside the build artifact to eyeball whether both the app itself and the major SDKs carry a privacy manifest.
# List the manifests inside the exported .appfind MyApp.app -name "*.xcprivacy" -print# e.g. MyApp.app/PrivacyInfo.xcprivacy# MyApp.app/Frameworks/GoogleMobileAds.framework/PrivacyInfo.xcprivacy
If your own app's manifest does not show up here, the app.config.ts declaration did not make it into the build. If an SDK's is missing, that SDK is non-compliant or out of date. After adding this one step, I almost stopped receiving warning emails.
Stop eyeballing it and return an exit code instead
Eyeballing gets forgotten, though. There is a lot to check before a release, and eventually you skim the list, decide it "looked fine," and move on. Partway through I started inserting this script.
#!/usr/bin/env bash# verify-privacy-manifests.sh — pre-submission bundling check# usage: ./verify-privacy-manifests.sh path/to/MyApp.appset -euo pipefailAPP_PATH="${1:?pass the path to the .app}"REQUIRED_SDKS=("GoogleMobileAds" "ExpoModulesCore")missing=0APP_MANIFEST="${APP_PATH}/PrivacyInfo.xcprivacy"if [ ! -f "${APP_MANIFEST}" ]; then echo "FAIL: no app-level manifest (app.config.ts did not reach the build)" missing=1else echo "OK: app-level PrivacyInfo.xcprivacy" # reject the file-exists-but-zero-declarations case if ! /usr/libexec/PlistBuddy -c "Print :NSPrivacyAccessedAPITypes:0" \ "${APP_MANIFEST}" >/dev/null 2>&1; then echo "FAIL: NSPrivacyAccessedAPITypes is empty (shipping it will not clear the warning)" missing=1 fififor sdk in "${REQUIRED_SDKS[@]}"; do if find "${APP_PATH}/Frameworks" -path "*${sdk}*" -name "*.xcprivacy" 2>/dev/null | grep -q .; then echo "OK: ${sdk}" else echo "FAIL: no manifest found for ${sdk} (possibly an outdated version)" missing=1 fidoneexit "${missing}"
A note on why it is written this way.
set -euo pipefail is there so a forgotten argument or a mid-script failure is never treated as a pass. A check script that quietly reports success is the worst outcome; I would rather it fall over.
REQUIRED_SDKS is an array so that adding one SDK is a one-line change. Rork projects accumulate dependencies with every feature, and keeping this spot easy to edit is what makes the habit survive.
The PlistBuddy read of the first element exists to catch a manifest that ships but contains nothing. Get the app.config.ts shape wrong and the file can be generated with no declarations inside — and a find listing will look perfectly healthy. That is exactly the case I missed first time around.
Returning exit "${missing}" means that even while you still run it by hand, you can later hang it off an EAS Build post-build hook or CI without rewriting anything.
Build it once and operation is light
A privacy manifest takes a lot of looking-up the first time, but once it is written correctly in app.config.ts it keeps shipping automatically on every build. All you need to check before release is whether a new feature trips a new Required Reason API.
Not stalling in review is unglamorous but ties straight to revenue. Lose a few days to a resubmission and that much AdMob impression and paid opportunity slides back. On your next release, start by running the verification script once against the exported .app. If it passes, submit; if it fails, the script names the cause for you.
Few things are as costly as days lost to a stalled review. I hope this helps you claw those days back.
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.