There is a category of app defect that no amount of code review finds, because the code is fine. You find it by launching the app.
We had one. A React Native app of ours would not start on iOS 27 — not a rendering glitch, not a blank screen, a hard termination at launch with EXC_BREAKPOINT inside a runtime check with the self-explanatory name _UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption.
The app had never been run on a device from this repository. Every test passed. The build succeeded. It crashed on every launch.
What changed in iOS
Apple introduced the UIScene lifecycle back in iOS 13, alongside iPad multitasking. For years it was optional: an app could keep using the older UIApplicationDelegate window model, Apple would note the deprecation, and everything kept working.
That grace period is over. On iOS 27, an app that has not adopted scenes is terminated at launch. The runtime check is explicit about why, which is a kindness — the crash symbol tells you exactly what is missing.
The specific requirement is a UIApplicationSceneManifest in the app’s Info.plist, plus a scene delegate that creates the window. An app that opens a bare UIWindow from application(_:didFinishLaunchingWithOptions:) with no manifest has not adopted scenes, regardless of how modern the rest of it is.
The part that surprised us
This was not legacy code. The native project was generated from the Expo SDK 57 template, current at the time, and that template still produced the old arrangement — a bare UIWindow, no scene manifest.
That is worth sitting with if you maintain React Native apps. The generated native project is not a permanently correct artefact. It is a snapshot of what the framework’s authors thought a native iOS project should look like on the day your version was cut, and platform requirements move underneath it. A project scaffolded six months ago against an SDK that was current then can be non-compliant now, and nothing in your JavaScript will tell you.
★ Insight -------------------------------------
In a managed React Native workflow the native project is usually regenerated rather than edited, which is what makes this class of bug invisible in review — there is no diff, because nobody wrote the offending code. It arrived with the template. The only reliable signal is running the app on the OS version your users are about to be on, which is exactly the check a JavaScript-only test suite is structurally unable to perform.
-------------------------------------------------
The fix
Expo’s documented path for this is a build property, ios.enableSceneSupport, which makes the generated native project adopt the scene lifecycle properly. It is documented at expo.fyi/ios-scene-lifecycle, and using it is better than hand-patching the Info.plist, because a hand patch is reverted the next time the native project is regenerated.
The flag requires a newer SDK patch than the one we were on, so the upgrade was:
| Package | From | To |
|---|---|---|
| expo | 57.0.16 | 57.0.24 |
| expo-build-properties | — | 57.0.21 |
| react-native | — | 0.86.3 |
| jest-expo | — | 57.0.5 |
One note for anyone doing this later: enableSceneSupport is a transitional flag. At SDK 58 it becomes a no-op because scene adoption is the default. Leaving it in place will not break anything, but it will sit in your config as an unexplained setting, so it is worth a comment saying when to delete it. Configuration whose reason for existing is undocumented is how projects accumulate settings nobody dares remove.
The dependency resolution that blocked it
expo install --fix — the command that is supposed to bring a project’s packages to versions compatible with its SDK — could not resolve this on its own.
The failure was an npm ERESOLVE. jest-expo pinned React Native’s Jest preset to 0.86.2 while the upgrade wanted React Native 0.86.3. Neither package was wrong; the peer dependency range simply had not caught up, and npm refused to pick a winner.
The way through was a clean install of the whole set rather than an incremental one — remove the lockfile and node_modules, install the target versions together, let the resolver solve the problem once instead of trying to patch an existing tree into a valid state.
That is generally the right instinct with ERESOLVE. An incremental install is being asked to find a path from a working state to a working state where every intermediate step also resolves, and sometimes no such path exists. A clean install only has to find the destination.
Resisting --legacy-peer-deps here matters. It would have made the error disappear without making the conflict disappear, leaving a tree where two packages disagree about which React Native they are compiled against — a class of problem that surfaces at runtime, on device, in a way that looks nothing like a dependency issue.
What we verified, and what we could not
Verification is where most upgrade write-ups get vague, so here is the exact state.
Verified: 460 tests pass. The Android release build was rebuilt after the upgrade and both end-to-end flows are green. The iOS app builds and launches on Xcode 27 and iOS 27 — which is the specific thing that was broken, so it is the specific thing that had to be confirmed.
Not verified: a full interactive pass on iOS. Our end-to-end driver hangs on iOS 27 and the device bridge it depends on crashes on start. Both are tooling problems rather than app problems, and neither has a workaround we were willing to trust.
We are stating that gap rather than rounding it up to “tested on iOS”, because an upgrade report that does not distinguish between “it launches” and “we drove every screen” is not much use to the person reading it later.
How to check your own app in five minutes
You do not need to reproduce the crash to know whether you are exposed.
1. Look for the scene manifest. Open your generated iOS project and search Info.plist for UIApplicationSceneManifest. If the key is absent, you have not adopted scenes, regardless of your framework version.
2. Check how the window is created. If your app delegate creates a UIWindow in application(_:didFinishLaunchingWithOptions:) and there is no scene delegate, that is the old model.
3. Check the flag, not the SDK number. Being on a current SDK is not the same as having scene support switched on. In Expo that means ios.enableSceneSupport in your build properties until SDK 58 makes it the default; in a bare React Native project it means the manifest and delegate exist in your own native source.
4. Launch it. On the newest OS you can install, on a device or a simulator. Thirty seconds, and it is the only step that gives you an answer rather than an inference.
If your app is managed and you have not regenerated the native project in months, run the check against a fresh generation too — what your framework produces today may differ from what is sitting in your repository.
The lesson worth generalising
Three separate defects came out of this work, and all three were found by running the app rather than by reading it. The app had a test suite. The test suite passed. The tests were testing behaviour written in JavaScript, and every one of these problems lived in the boundary between that JavaScript and the platform underneath it.
If you maintain a React Native or Expo app and have not launched it on the current OS beta, that is the highest-value hour available to you. Not a simulator smoke test buried in CI — open it, on the OS your users will be on in a month, and use it.
The platform will not wait for your release cycle, and the generated native project that was correct when you scaffolded it is not correct forever.
Awesome Apps is an app developer in Sydney that builds and maintains iOS and Android apps for Australian businesses. If an upgrade like this one is overdue, book a call. Websites come from our sister brand Cosmos Web Tech, cloud and IT from Cloud Geeks, and we are part of Ganda Tech Services.
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