Skip to content

showOverlay creates a scene-less UIWindow, so overlays never appear under UIScene lifecycle #8345

Description

@sh-bnsl

What happened?

On iOS, Navigation.showOverlay() creates a new RNNOverlayWindow with initWithFrame: and never assigns a UIWindowScene.

With a scene-based app lifecycle, that window is not added to the scene, so the overlay never appears. JS still mounts and runs (the overlay VC is created and rendered), which makes this look like a hang: native/RNN splash dismisses, overlay logic is running, but the screen stays on RNNSplashScreenViewController / the current root.

Navigation.setRoot() is not affected, because it attaches the layout to the existing _mainWindow that the host already bound to the scene.

This is easy to miss on older iOS if the app still used the AppDelegate window path. It becomes a hard break once the app adopts UIWindowScene (mandatory on iOS 26+).

What was the expected behaviour?

showOverlay shows the overlay above the current root on the same UIWindowScene.

Was it tested on latest react-native-navigation?

  • I have tested this issue on the latest react-native-navigation release and it still reproduces.

Help us reproduce this issue!

  1. Use UIScene lifecycle (UIApplicationSceneManifest + UIWindowSceneDelegate).
  2. Create the window with initWithWindowScene: and pass it to RNN (delegate.window set before bootstrap).
  3. On Navigation.events().registerAppLaunchedListener, do not setRoot. Call:
Navigation.showOverlay({
  component: {
    name: 'example.OverlayScreen',
  },
});
  1. OverlayScreen mounts (componentDidMount / useEffect runs) but nothing is visible. The scene window is still RNNSplashScreenViewController.

Control: the same screen shown via setRoot appears correctly:

Navigation.setRoot({
  root: {
    stack: {
      children: [{ component: { name: 'example.OverlayScreen' } }],
    },
  },
});

In what environment did this happen?

  • react-native-navigation: 8.8.9
  • react-native: 0.87 (New Architecture / Fabric, bridgeless)
  • iOS: 26 / 27 (also repros conceptually on iOS 13+ with UIScene)
  • Host: RNNAppDelegate + custom UIWindowSceneDelegate
  • Platform: iOS only

Activity

  1. sh-bnsl commented on Aug 27, 2026

    @sh-bnsl
    Author

    Root cause

    RNNCommandsHandler.showOverlay: (current):

    [overlayVC setReactViewReadyCallback:^{
      UIWindow *overlayWindow =
          [[RNNOverlayWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
      overlayWindow.rootViewController = weakOverlayVC;
      [self->_overlayManager showOverlayWindow:overlayWindow];
      // ...
    }];
    [overlayVC render];

    RNNOverlayManager then only does setHidden:NO. That is not enough for a window with no scene.

    Compare with setRoot:, which mutates the already scene-connected _mainWindow:

    [vc setReactViewReadyCallback:^{
      self->_mainWindow.rootViewController = weakVC;
    }];

    Extra detail (New Arch): overlay VCs do not set waitForRender, so reactViewReadyCallback fires immediately and then creates this window. This is not “callback never called”. The callback creates the wrong kind of window.

    ReactNativeNavigation.mainWindow can also create a scene-less window if delegate.window is nil:

    keyWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

    That is a related footgun for scene-based hosts, but the overlay bug happens even when _mainWindow is already scene-connected.

    Suggested fix

    Create overlay windows with the main window’s scene:

    RNNOverlayWindow *overlayWindow;
    UIWindowScene *windowScene = self->_mainWindow.windowScene;
    if (windowScene) {
      overlayWindow = [[RNNOverlayWindow alloc] initWithWindowScene:windowScene];
      overlayWindow.frame = self->_mainWindow.bounds;
    } else {
      overlayWindow = [[RNNOverlayWindow alloc] initWithFrame:self->_mainWindow.bounds];
    }
    overlayWindow.rootViewController = weakOverlayVC;

    We patched this locally against 8.8.9 and overlays became visible. Using _mainWindow.windowScene is important so the overlay lands on the same scene RNN already owns.

    Please also consider:

    1. Same initWithWindowScene: path for any other extra windows RNN creates.
    2. Avoid initWithFrame: fallback on iOS 13+ when a scene exists — that window will stay invisible.
    3. In mainWindow, if delegate.window is missing, prefer a connected UIWindowScene over initWithFrame:.
  2. changed the title [-]`showOverlay` creates a scene-less `UIWindow`, so overlays never appear under UIScene lifecycle (iOS 13+, required on iOS 26/27)[/-] [+]`showOverlay` creates a scene-less `UIWindow`, so overlays never appear under UIScene lifecycle[/+] on Aug 27, 2026
  3. MrKinq commented on Sep 16, 2026

    @MrKinq

    Root cause

    RNNCommandsHandler.showOverlay: (current):

    [overlayVC setReactViewReadyCallback:^{
    UIWindow *overlayWindow =
    [[RNNOverlayWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    overlayWindow.rootViewController = weakOverlayVC;
    [self->_overlayManager showOverlayWindow:overlayWindow];
    // ...
    }];
    [overlayVC render];
    RNNOverlayManager then only does setHidden:NO. That is not enough for a window with no scene.

    Compare with setRoot:, which mutates the already scene-connected _mainWindow:

    [vc setReactViewReadyCallback:^{
    self->_mainWindow.rootViewController = weakVC;
    }];
    Extra detail (New Arch): overlay VCs do not set waitForRender, so reactViewReadyCallback fires immediately and then creates this window. This is not “callback never called”. The callback creates the wrong kind of window.

    ReactNativeNavigation.mainWindow can also create a scene-less window if delegate.window is nil:

    keyWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    That is a related footgun for scene-based hosts, but the overlay bug happens even when _mainWindow is already scene-connected.

    Suggested fix

    Create overlay windows with the main window’s scene:

    RNNOverlayWindow *overlayWindow;
    UIWindowScene *windowScene = self->_mainWindow.windowScene;
    if (windowScene) {
    overlayWindow = [[RNNOverlayWindow alloc] initWithWindowScene:windowScene];
    overlayWindow.frame = self->_mainWindow.bounds;
    } else {
    overlayWindow = [[RNNOverlayWindow alloc] initWithFrame:self->_mainWindow.bounds];
    }
    overlayWindow.rootViewController = weakOverlayVC;
    We patched this locally against 8.8.9 and overlays became visible. Using _mainWindow.windowScene is important so the overlay lands on the same scene RNN already owns.

    Please also consider:

    1. Same initWithWindowScene: path for any other extra windows RNN creates.
    2. Avoid initWithFrame: fallback on iOS 13+ when a scene exists — that window will stay invisible.
    3. In mainWindow, if delegate.window is missing, prefer a connected UIWindowScene over initWithFrame:.

    I hit the same mainWindow footgun on scene-based apps and fixed it locally in ReactNativeNavigation.mm by preferring an already connected UIWindowScene instead of creating a scene-less UIWindow with initWithFrame:.

    This is the patch I’m currently using:

    - (UIWindow *)mainWindow {
        UIWindow *keyWindow = UIApplication.sharedApplication.delegate.window;
    
        if (keyWindow) {
            return keyWindow;
        }
    
        if (@available(iOS 13.0, *)) {
            for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
                if (![scene isKindOfClass:[UIWindowScene class]]) {
                    continue;
                }
    
                UIWindowScene *windowScene = (UIWindowScene *)scene;
    
                if (windowScene.activationState == UISceneActivationStateUnattached) {
                    continue;
                }
    
                keyWindow = [[UIWindow alloc] initWithWindowScene:windowScene];
                keyWindow.frame = windowScene.coordinateSpace.bounds;
    
                UIApplication.sharedApplication.delegate.window = keyWindow;
                return keyWindow;
            }
        }
    
        // Legacy fallback only. With UIScene configured this should not normally be reached on iOS 13+ once the scene has connected.
        keyWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        UIApplication.sharedApplication.delegate.window = keyWindow;
    
        return keyWindow;
    }
    

    With this change, the RNN main window is attached to a real UIWindowScene when delegate.window is still nil.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions