Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions samples/expo/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ yarn android # Build & run the Android dev client (expo run:android)
```

`yarn start` then follows the Expo CLI prompts to open on an iOS simulator, Android emulator, or a physical device. Because the SDK ships native code, plain Expo Go isn't enough — the `run:*` scripts build a dev client that includes it.

## iOS UIScene lifecycle

The `plugins/withIosSceneLifecycle.js` config plugin makes this app adopt the iOS **UIScene lifecycle** with Expo's own `EXExpoAppSceneDelegate` (a Swift `UIWindowSceneDelegate`). This is where Apple/Expo are pushing every app ("Required by iOS 27"), so running the sample this way keeps the SDK exercised under the lifecycle real apps are migrating to. Expo 57 ships `ExpoAppSceneDelegate` but does not wire it up during prebuild yet, so the plugin adds the `UIApplicationSceneManifest` and adjusts the generated `AppDelegate` (conform to `ExpoReactNativeFactoryProvider`, and let the scene delegate own the window / React Native start).
3 changes: 2 additions & 1 deletion samples/expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
}
}
],
"expo-web-browser"
"expo-web-browser",
"./plugins/withIosSceneLifecycle"
],
"extra": {
"router": {
Expand Down
76 changes: 76 additions & 0 deletions samples/expo/plugins/withIosSceneLifecycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Makes the Expo sample adopt the iOS UIScene lifecycle with Expo's own `EXExpoAppSceneDelegate`
* (a Swift `UIWindowSceneDelegate`). This is the direction Apple/Expo are pushing every app
* ("Required by iOS 27"), so running the sample this way keeps the SDK exercised under the
* lifecycle real apps are migrating to.
*
* Expo 57 ships `ExpoAppSceneDelegate` but does not yet wire it up during prebuild, so this plugin
* does it:
* 1. Adds a `UIApplicationSceneManifest` to Info.plist pointing at `EXExpoAppSceneDelegate`.
* 2. Makes the generated `AppDelegate` conform to `ExpoReactNativeFactoryProvider` and defer the
* window / React Native start to the scene delegate.
*/
const { withInfoPlist, withAppDelegate } = require('@expo/config-plugins');

const SCENE_DELEGATE_CLASS = 'EXExpoAppSceneDelegate';

function withSceneManifest(config) {
return withInfoPlist(config, cfg => {
cfg.modResults.UIApplicationSceneManifest = {
UIApplicationSupportsMultipleScenes: false,
UISceneConfigurations: {
UIWindowSceneSessionRoleApplication: [
{
UISceneConfigurationName: 'Default Configuration',
UISceneDelegateClassName: SCENE_DELEGATE_CLASS,
},
],
},
};
return cfg;
});
}

function patchAppDelegate(contents) {
let out = contents;

// Conform to ExpoReactNativeFactoryProvider so the scene delegate can retrieve the factory the
// app delegate creates in didFinishLaunching.
const classDecl = 'class AppDelegate: ExpoAppDelegate {';
if (!out.includes(classDecl)) {
throw new Error('[withIosSceneLifecycle] AppDelegate class declaration not found; Expo template changed.');
}
out = out.replace(classDecl, 'class AppDelegate: ExpoAppDelegate, ExpoReactNativeFactoryProvider {');

// Remove the app-delegate window creation + RN start; under the scene lifecycle the scene
// delegate (ExpoAppSceneDelegate) owns the window and starts React Native.
const windowBlock = /#if os\(iOS\) \|\| os\(tvOS\)[\s\S]*?factory\.startReactNative\([\s\S]*?\)\s*#endif/;
if (!windowBlock.test(out)) {
throw new Error('[withIosSceneLifecycle] window/startReactNative block not found; Expo template changed.');
}
out = out.replace(
windowBlock,
[
'// [withIosSceneLifecycle] Scene lifecycle: ExpoAppSceneDelegate creates the window and',
' // starts React Native in scene(_:willConnectTo:). Do NOT start it here.',
].join('\n'),
);

return out;
}

function withSceneAppDelegate(config) {
return withAppDelegate(config, cfg => {
if (cfg.modResults.language !== 'swift') {
throw new Error('[withIosSceneLifecycle] expected a Swift AppDelegate.');
}
cfg.modResults.contents = patchAppDelegate(cfg.modResults.contents);
return cfg;
});
}

module.exports = function withIosSceneLifecycle(config) {
config = withSceneManifest(config);
config = withSceneAppDelegate(config);
return config;
};
Loading