Here is a bug that cannot be found by reading either of the two files involved, because each one is correct on its own.

File one, the app config: orientation: "portrait".

File two, a screen that displays a card table: on mount, ask the OS to rotate to landscape.

Both are reasonable. The app is portrait almost everywhere, so declaring portrait is the obvious default. The table needs width, so asking for landscape is the obvious implementation. Put them together and the table screen could never have rotated on iPhone, not once, since the day it was written.

Why iOS silently refuses

orientation: "portrait" in an Expo config writes UISupportedInterfaceOrientations into the app’s Info.plist with portrait as the only entry.

That key is not a preference. It is the exhaustive set of orientations the app is permitted to use. A runtime request to rotate to an orientation that is not in that list is not an error — iOS simply does not honour it. No exception, no warning, no log line. The request is made, the screen stays portrait, and every piece of code involved reports success.

Android behaves differently, and that difference is what let this survive. An Android activity may change its orientation at runtime regardless of what the manifest declares as a default. On Android the table rotated, looked right, and worked exactly as designed. The feature was verified — on the platform where it could not fail.

★ Insight ------------------------------------- The dangerous shape here is not “iOS is stricter than Android”. It is that the strictness is expressed as silence. A permission model that throws teaches you its rules the first time you break them; one that ignores you teaches you nothing, and a cross-platform test suite that runs on one platform will confirm whatever that platform allows. Any behaviour where the two OSes differ in what they permit is worth an explicit test, because the looser platform will mask the tighter one indefinitely. -------------------------------------------------

The fix, and why it is not “declare landscape”

The obvious repair is to add landscape to the declaration. That is half right and it is the half that causes the next bug.

Declaring both orientations means every screen can now rotate, including the ones designed only for portrait. Turn the phone sideways on a form and you get a layout nobody ever looked at.

What we changed it to is orientation: "default", which declares that the app supports rotation, combined with per-screen locking — each screen states what it needs, and the table screen is the only one asking for landscape.

The declaration is a capability. The lock is a policy. Conflating them is what produced the original bug in one direction and produces the layout bug in the other.

The test that stops it recurring

A fix that relies on nobody making the same mistake again is not a fix. So the repair came with a contract test that fails the build on two conditions:

A screen locks an orientation the app does not declare. This is the original bug, expressed as a rule. The test reads what the app config declares and what every screen requests, and fails when a screen asks for something outside the declared set. It would have failed on the original code.

A screen locks landscape without restoring portrait. This is the bug we would have shipped next. A screen that rotates to landscape and does not unlock on unmount leaves the whole app sideways — the user navigates back to a list that was only ever designed as a column, and the app looks broken in a way that is hard to attribute to the screen that caused it.

Neither of these is a unit test of a function. They are assertions about the relationship between a configuration file and a directory of screens, which is exactly the kind of thing that has no natural home and therefore usually goes unchecked.

What a contract test looks like

It is worth being concrete, because “add a test” is advice nobody can act on.

The test does not launch anything and does not render a screen. It reads two sources and compares them:

  • The declared set. Parse the app config for the orientation declaration and expand it to the orientations the build will actually permit.
  • The requested set. Walk the screens directory, find every runtime orientation call, and record which screen asks for what.

Then it asserts two rules. Every requested orientation is a member of the declared set. And every screen that locks also unlocks — the lock and its release are both present in the same file.

The reason this belongs in your test suite rather than in a code review checklist is that the two sources live far apart. The declaration is one line in a config file that nobody opens. The request is one line in a screen that somebody is editing for an unrelated reason. Nothing in either file mentions the other, so there is no moment during normal work at which a person would think to check.

That is the general shape worth reusing: when correctness depends on two files agreeing, and neither file references the other, a test comparing them is usually the only thing that will ever notice.

Three more things rotation broke

Once the table could actually rotate, we found the layout underneath had never been seen on a phone.

The score chips sat under the status bar clock. In portrait the top inset is the notch area and the layout accounted for it. In landscape the insets move to the sides and the top shrinks — a layout using a fixed top padding puts content under the system clock.

The demo banner drew on top of another player’s seat. Same cause: a position that is clear in one aspect ratio and overlapping in the other.

Horizontal insets were missing entirely on three screens. This one is not about landscape at all, and it is the one most apps have. A rotated device reports a non-zero left or right safe-area inset, and so does a folded one. A layout with a fixed horizontal padding and no inset handling puts its content under the hardware — a rounded corner, a camera housing, or the hinge.

That third one is worth a check in your own app today, independent of any of this. Rotate the phone on your main screens and look at the edges. Fixed horizontal padding is close to universal in React Native layouts and it is wrong on every device with a non-rectangular screen, which is now most of them.

Where to look in your own app

Four questions, in order of how likely they are to turn something up.

  1. Does your app config declare a single orientation? If yes, search the codebase for any runtime orientation call. Every one of them is either dead code or a silent failure on iOS.
  2. Does any screen lock an orientation without unlocking it? Navigate away from that screen and see what the next one looks like.
  3. Was the last person to test the rotating screens on Android? If so, nothing has been verified about iOS.
  4. Do your layouts read horizontal safe-area insets, or use a fixed padding? Rotate and look at the edges.

The general form of all four: when two platforms disagree about what is permitted, the permissive one will hide your bug for as long as you let it be the platform you test on.


Awesome Apps builds iOS and Android apps for Australian businesses. Web work comes from Cosmos Web Tech, cloud and managed IT from Cloud Geeks, and we are part of Ganda Tech Services.

Ready to build your app?

Talk to a Sydney app developer — free.

30 minutes. We'll tell you what your app needs, how long it takes, and what it costs. Real answers, no sales pitch.

Book Free App Strategy Call →

Free · 30 minutes · No obligation