You build an app with Rork, load it on your phone, turn the device sideways — and nothing happens. Or the opposite: you want the login screen to stay in portrait but it keeps flipping whenever the user tilts the phone. If this sounds familiar, you're not alone. I ran into this exact problem when building my first game-style app in Rork and wanted the gameplay screen locked to landscape while everything else stayed portrait.
By default, Rork's AI generates apps in portrait-only mode, and retrofitting rotation support — especially per-screen control — exposes some unexpected gotchas on both iOS and Android. This guide walks through the real traps in order, so you can stop guessing and ship.
Start Here: The orientation Field in app.json
Open the project Rork generated and look at app.json (or app.config.js). You'll see something like this:
{
"expo": {
"orientation": "portrait",
"ios": { /* ... */ },
"android": { /* ... */ }
}
}The orientation field sets the app-wide default. It accepts three values:
"portrait"— locked to portrait (the default Rork generates)"landscape"— locked to landscape"default"— follows the device, both orientations supported
If nothing rotates at all, this is almost always where the issue is. Change it to "default" and rebuild.
{
"expo": {
"orientation": "default"
}
}Simple so far — but orientation never stays this clean for long. The setting interacts with native manifests, device-level preferences, and per-screen overrides, which is where most debugging hours disappear.
The iOS Gotcha: Build Cache and iPad Requirements
Even with "default" set, you might still see iOS refuse to rotate. The culprit is usually a stale build cache. Under the hood, Expo regenerates the iOS Info.plist entries (specifically UISupportedInterfaceOrientations) from app.json at build time, but a cached build from before your change can hide the new values. I've shipped a build twice in a row thinking my change didn't take, only to realize I was testing the previous binary.
# Clear the cache and rebuild
eas build --platform ios --clear-cacheIf you want only iPad to support all orientations while keeping iPhone in portrait, you have to say so explicitly in the ios section:
{
"expo": {
"orientation": "portrait",
"ios": {
"requireFullScreen": false,
"infoPlist": {
"UISupportedInterfaceOrientations~ipad": [
"UIInterfaceOrientationPortrait",
"UIInterfaceOrientationPortraitUpsideDown",
"UIInterfaceOrientationLandscapeLeft",
"UIInterfaceOrientationLandscapeRight"
]
}
}
}
}The requireFullScreen: false line is what makes your app play nicely with Slide Over and Split View on iPad. Apple's reviewers often flag apps that don't support iPad multitasking — worth setting proactively if you ship to both device classes, because getting rejected at review is far more painful than adding one line now. In addition, Apple's Human Interface Guidelines strongly encourage iPad apps to support all four orientations, and reviewers have started rejecting apps that appear locked to portrait on larger screen sizes, even without explicit multitasking complaints.
If you want to test the generated Info.plist before submitting, run eas build --platform ios --profile development locally, then extract the .ipa, rename it to .zip, and inspect the Info.plist inside the Payload/*.app/ directory. It's an extra step, but it beats finding out at the review stage.
Switching Orientation Per Screen
"Most of the app is portrait, but the video player should be landscape." This is a common requirement in media and game apps, and the tool for the job is expo-screen-orientation.
Install it via Rork's package panel, or locally:
npx expo install expo-screen-orientationThen, in the screen that needs a different orientation, lock on entry and restore on exit:
import * as ScreenOrientation from 'expo-screen-orientation';
import { useEffect } from 'react';
export default function VideoPlayerScreen() {
useEffect(() => {
// Lock to landscape when this screen mounts
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.LANDSCAPE
);
// Restore portrait on unmount
return () => {
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
);
};
}, []);
return (/* your video player JSX */);
}The cleanup function is essential. Skip it and the previous screen ends up stuck in landscape too — a confusing UX bug that's easy to miss in development, because developers usually test by navigating forward, not backward.
There's one more subtlety worth calling out: if app.json still says "orientation": "portrait", iOS will silently refuse the lockAsync(LANDSCAPE) call. iOS only honors orientations the app has declared support for at the manifest level. For per-screen control to work, the app-wide setting must be "default", and you enforce the portrait-lock on other screens individually with OrientationLock.PORTRAIT_UP.
For navigation libraries like React Navigation or Expo Router, a cleaner pattern is to wrap the orientation call in a useFocusEffect, so the lock fires whenever the screen is focused — not just on mount. This matters if users can return to the screen via back navigation without re-mounting it.
Android-Specific Checks
Sometimes iOS rotates just fine, but Android stays stubbornly in portrait. The usual cause is that android:screenOrientation in the generated manifest still reads portrait instead of sensor or fullSensor.
When you're under Expo's managed workflow, app.json drives this and should regenerate correctly. But if you've ever done a local build or hand-edited the manifest, it's worth double-checking. One way to confirm is to unzip the APK and inspect AndroidManifest.xml directly — if it still says portrait, the rebuild didn't pick up your change.
Another frequent cause on Android 11 and newer: the user has "auto-rotate" turned off in system settings. Your app will honor "default" but still behave as if it's locked to portrait, because the OS blocks rotation globally. This catches people often during beta testing, where a tester reports "rotation is broken" but the device is the problem.
// Quick check: is the orientation even supported on this device right now?
import * as ScreenOrientation from 'expo-screen-orientation';
async function checkOrientationSupport() {
const supported = await ScreenOrientation.supportsOrientationLockAsync(
ScreenOrientation.OrientationLock.LANDSCAPE
);
console.log('Landscape supported:', supported);
// → false usually means a system-level setting is blocking rotation
}When supportsOrientationLockAsync returns false, stop debugging your code and check the device settings first. I've lost an hour to this one myself — writing extra try/catch handlers and logging statements before realizing the phone's rotation lock was on.
When Rotation Works, But the Layout Breaks
Congratulations — your app rotates. Now you're facing the next wall: the layout was designed for portrait, and in landscape everything overflows or crams awkwardly into a corner.
Rork's AI usually generates layouts assuming a tall aspect ratio. The cleanest fix is the useWindowDimensions hook — it reports the current dimensions and updates on rotation, unlike Dimensions.get('window') which returns a stale initial value.
import { useWindowDimensions, View, StyleSheet } from 'react-native';
export default function ResponsiveScreen() {
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
return (
<View style={[
styles.container,
isLandscape ? styles.landscape : styles.portrait,
]}>
{/* your content */}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
portrait: { flexDirection: 'column' },
landscape: { flexDirection: 'row', paddingHorizontal: 32 },
});Remember: useWindowDimensions, not Dimensions.get. The latter only gives you the value at mount time and won't update when the device rotates.
One more tip: when your layout has multiple nested views, resist the urge to add aspectRatio constraints everywhere. aspectRatio locks the dimension relationship and tends to cause clipping when the orientation changes mid-animation. Prefer percentage-based width / height with flex weights — they adapt more gracefully to mid-transition frames.
If your app has images or video that must maintain their own aspect ratio during rotation, use resizeMode: 'contain' on the Image component rather than forcing the container to match. The image adapts, the container stays flexible.
For screens with a lot of conditional layout, I find it cleaner to derive an orientation variable at the top of the component and pass it into stylesheet selectors, rather than sprinkling isLandscape ternaries throughout the JSX. It keeps the template readable when the layout has many branches.
A Sanity Check Before Shipping
Orientation bugs have a nasty habit of appearing only in specific transitions — you rotate while a modal is open, or you rotate then navigate, or you background the app in landscape and foreground it. In my experience, most orientation-related review rejections come from these transition edge cases rather than the steady state.
Before release, I work through this short list on a real device (not just the simulator):
- Rotate while on every screen, including modals and overlays
- Rotate, then navigate forward and back — does the orientation lock restore correctly?
- Rotate, send the app to the background, rotate the device back, and foreground — does the UI come up in the correct orientation?
- On iPad, test Split View with the app occupying both the narrower and wider pane
- On Android, test with auto-rotate off in system settings (the app should still behave sensibly, even if it can't rotate)
The simulator is convenient for quick checks but misses behaviors tied to real sensors and OS-level rotation locks. A 10-minute pass on a physical device catches almost everything.
Still Stuck? A Quick Checklist
If rotation still isn't behaving after the changes above, work through these in order:
- Test on an
eas buildoutput, not on the Rork Preview / Companion app — Preview handles orientation locks differently than a production build, so Preview passing doesn't guarantee the release build will - After any
app.jsonchange, rebuild with--clear-cacheto bust the build cache - When using
expo-screen-orientation, confirmapp.jsonhas"orientation": "default"— per-screen locks are no-ops on iOS if the app-level setting forbids the orientation - For iPad, confirm
ios.requireFullScreen: falseis set to enable multitasking - On Android, check that the device's system "auto-rotate" is enabled before blaming your code
If you're also seeing navigation-side issues, Fixing Expo Router Screen Navigation Errors in Rork may help. And if device-specific crashes are part of the same debugging session, Simulator vs. Real-Device Crash Triage in Rork Max covers the broader cross-device workflow.
Screen orientation looks like a one-line config change, but the real gotchas hide in per-screen control, iPad-specific flags, and device-level settings. The cleanest mental model is: start with the app-wide setting in app.json, layer per-screen control on top when needed, then adapt the layout with useWindowDimensions. Before your next session, decide one thing — what rotation policy does your app actually need? Full rotation, portrait-only except one screen, or tablet-only landscape? Once that's settled, the implementation above takes you the rest of the way.