●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Rork generates Expo / React Native apps. A practical look at architecture, surfacing crashes in production, list performance, the eas submit workflow, and the iOS review pitfalls to clear — learned from real review round-trips.
Building production iOS apps with Rork requires more than tutorials. Scalable architecture, crash monitoring, list performance, and a steady submission workflow are what actually decide whether your release goes smoothly.
Here are patterns validated at real production scale, step by step.
MVVM Architecture
Rork app scalability depends on proper architecture design.
class ProductListViewModel { @Published var products: [Product] = [] @Published var isLoading = false @Published var error: Error? private let service: ProductService init(service: ProductService) { self.service = service } @MainActor func loadProducts() async { isLoading = true defer { isLoading = false } do { products = try await service.fetchProducts() } catch { self.error = error } }}struct ProductListView: View { @StateObject var viewModel: ProductListViewModel var body: some View { List(viewModel.products) { product in ProductRow(product: product) } .task { await viewModel.loadProducts() } }}
Performance Optimization
struct OptimizedProductList: View { @StateObject var viewModel: ProductListViewModel var body: some View { List { ForEach(viewModel.products, id: \.id) { product in ProductRow(product: product) .onAppear { viewModel.prefetchProduct(after: product) } } } }}class ImageCache { static let shared = ImageCache() private var cache: NSCache<NSString, UIImage> = NSCache() func image(for url: URL) -> UIImage? { return cache.object(forKey: url.absoluteString as NSString) }}
✦
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
✦Surface native and JS crashes (and white screens) in production with Sentry in Expo, then patch them over OTA
✦Ready-to-use FlatList and expo-image settings that keep list screens from re-rendering and bloating memory
✦A pre-submission checklist that cuts eas submit and App Store Connect round-trips
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.
class DataPersistence { static let shared = DataPersistence() func save<T: Codable>(_ object: T, for key: String) { if let encoded = try? JSONEncoder().encode(object) { UserDefaults.standard.set(encoded, forKey: key) } } func load<T: Codable>(_ type: T.Type, for key: String) -> T? { guard let data = UserDefaults.standard.data(forKey: key) else { return nil } return try? JSONDecoder().decode(T.self, from: data) }}
Rork Generates Expo / React Native Apps
The code above is native Swift/SwiftUI, but what Rork actually outputs is an Expo-managed React Native app. Most apps ship without writing any of that Swift. The native patterns matter when you reach for expo prebuild or a config plugin to add a native module, or rebuild a hot path natively.
In practice, the first thing you trip over in production isn't your Swift architecture — it's the operations and review specifics of an Expo app. The sections below cover what the intro guides skip, learned from actual round-trips between App Review and user bug reports.
Making Crashes and White Screens Visible in Production
The scariest thing in solo development is a crash that only happens on a user's device and never reproduces on yours. Your simulator launches fine every time, yet the review says "opens and immediately closes." Crash monitoring in production is what closes that distance.
In Expo, adding Sentry's official plugin to app.json collects both native crashes and JavaScript exceptions.
Initialize it once at the app entry point. Leaving tracesSampleRate at 1.0 in production gets noisy fast, so it's realistic to dial it down.
// App.tsx / app/_layout.tsximport * as Sentry from "@sentry/react-native";Sentry.init({ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN, // Keep production around 0.1–0.2; only crank to 1.0 while developing tracesSampleRate: __DEV__ ? 1.0 : 0.2, enableNativeCrashHandling: true,});export default Sentry.wrap(App);
The infamous "blank white screen" in React Native is usually an exception thrown mid-render that wipes the view. An Error Boundary catches it, gives the user a retry button, and sends the report to Sentry for you.
The key point: if the crash is on the JavaScript side, you can patch it the same day over OTA (expo-updates). Once Sentry shows you the failing line, fix the JS and run eas update — no native rebuild or review wait, and you rescue the users who are crashing. Crash monitoring and OTA only become powerful together.
Building List Screens That Don't Stutter
Most apps that get "janky" or "freezes when scrolling" reviews are tripping over a list screen. For long lists in React Native, use FlatList instead of mapping into a ScrollView, and keep re-renders and image loads in check.
import { FlatList } from "react-native";import { Image } from "expo-image";import { memo, useCallback } from "react";// Memoize the row so unrelated updates don't repaint itconst ProductRow = memo(function ProductRow({ item }: { item: Product }) { return ( <Image source={item.imageUrl} // expo-image has a built-in disk cache: faster re-display, stable memory cachePolicy="memory-disk" style={{ width: "100%", height: 200 }} contentFit="cover" transition={150} /> );});export function ProductList({ products }: { products: Product[] }) { const renderItem = useCallback( ({ item }: { item: Product }) => <ProductRow item={item} />, [] ); return ( <FlatList data={products} keyExtractor={(item) => String(item.id)} renderItem={renderItem} // Drop off-screen rows to keep memory down removeClippedSubviews initialNumToRender={8} maxToRenderPerBatch={8} windowSize={7} /> );}
Three things matter here. Wrap the row in memo so it's decoupled from parent updates, hand images to expo-image so caching and decoding are optimized, and use initialNumToRender and windowSize to cap how many rows you hold at once. For an image-heavy list like a wallpaper app, those three alone visibly change how smooth scrolling feels.
If your rows are a fixed height, adding getItemLayout skips scroll-position math and shaves off more. If the height varies, don't force it — let removeClippedSubviews do the work instead.
Cutting eas submit and App Store Connect Round-Trips
A build that's done can still eat a day if it stalls at submission. Pin your eas submit settings in eas.json so one command gets you to TestFlight.
# Build, then submit straight througheas build --platform ios --profile productioneas submit --platform ios --profile production --latest
A common snag on the App Store Connect side is the encryption declaration (Export Compliance). Even if you only use HTTPS, the dialog appears every time — but one line in app.json removes that manual step at each submission.
Add yourself as an internal TestFlight tester so you can do a final on-device check right after submitting. Before sending anything to review, I always go through TestFlight once just to watch the first launch and the push-notification permission dialog. Permission strings that never appear in the simulator are sometimes empty on a real device — and that's where you catch it.
Pre-Submission Checklist
Before pressing submit, walk through these one at a time, out loud. Cutting even one round-trip makes the release rhythm noticeably lighter.
Check
Where
What happens if you miss it
ATT purpose string (if you run ads)
app.json ios.infoPlist
Bounced under Guideline 5.1.1
Camera / photo permission strings
app.json ios.infoPlist
Empty strings get rejected
runtimeVersion pinned to appVersion
app.json
New JS on old binary crashes on launch
Auto-incrementing build number
eas.json production
Duplicate number blocks submission
Encryption declaration (usesNonExemptEncryption)
app.json ios.config
Manual dialog at every submission
Crash monitoring initialized
App entry point
You never see production white screens
First launch verified on a TestFlight device
App Store Connect
Empty permission strings slip through
iOS-Specific Pitfalls to Clear Before Submission
A bit more on the four from the checklist that trip people up most.
1. AdMob needs an ATT purpose string
If you add ads (AdMob), App Store review will block you without an App Tracking Transparency purpose string (NSUserTrackingUsageDescription). In Expo you set it directly in app.json under ios.infoPlist.
{ "expo": { "ios": { "infoPlist": { "NSUserTrackingUsageDescription": "We measure usage only with your permission, to improve ad relevance." } } }}
An empty or placeholder string often gets bounced under Guideline 5.1.1. Write a real, localized sentence for each market you ship to.
2. Permission strings are auto-added but left empty
When you use the camera or photo library through expo-image-picker and similar, the required NS...UsageDescription keys are added to Info.plist automatically — but not their text. Submitting with empty strings is a common rejection. Add one sentence per permission explaining why you need it.
3. Know what OTA (expo-updates) can't ship
JavaScript fixes go out instantly over OTA, but adding a native module or bumping the Expo SDK major version cannot. If runtimeVersion isn't set to { "policy": "appVersion" }, new JS can land on an old native binary and crash on launch. Treat OTA as "JS-only fixes."
4. Split your EAS Build profiles
Separate development / preview / production in eas.json, and let production auto-increment the build number so you never hand-bump it at submission time.
I've run wallpaper and relaxation apps as a solo developer since 2014, and AdMob is still the revenue backbone. That's exactly why forgetting the ATT purpose string once — and earning a review round-trip for it — was an expensive lesson. On iOS, a single line in Info.plist can stop a perfectly good build.
Next Step
Before your next eas build for production, walk the checklist above from top to bottom. Then, for the first few days after release, make a habit of opening the Sentry dashboard first thing in the morning — you'll catch white-screen reports before they pile up in your reviews. One fewer review round-trip, and a day faster on crash response, makes a solo release a much calmer thing.
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.