From a1ecfd00eb66bba1a6574c2a59a4e479b3887fa1 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sat, 10 Oct 2026 00:06:27 +0300 Subject: [PATCH 1/2] fix(ios): find the host window under the UIScene life cycle unloadUnity() and the old-arch view manager looked up the host window via UIApplication.delegate.window. Under the UIScene life cycle (required by the iOS 27 SDK, default in Expo SDK 58) the window belongs to the SceneDelegate and delegate.window is nil, so unloadUnity() skipped unloadApplication entirely and Unity stayed resident. RNUnityHostWindow() resolves the host window from the view itself, then the foreground scene's delegate window or key window, then the app delegate as before. unloadUnity() now unloads even when no host window is found. --- ios/RNUnityView.h | 5 +++++ ios/RNUnityView.mm | 46 +++++++++++++++++++++++++++++++++++---- ios/RNUnityViewManager.mm | 2 +- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/ios/RNUnityView.h b/ios/RNUnityView.h index 18afa00..176c6ba 100644 --- a/ios/RNUnityView.h +++ b/ios/RNUnityView.h @@ -1,8 +1,13 @@ #import #import +#import #include #include +// The host app's window. Under the UIScene life cycle (required by the iOS 27 SDK, +// default in Expo SDK 58) the app delegate owns no window, so `delegate.window` is nil. +FOUNDATION_EXTERN UIWindow * _Nullable RNUnityHostWindow(UIView * _Nullable view, UIWindow * _Nullable exclude); + // This guard prevent this file to be compiled in the old architecture. #ifdef RCT_NEW_ARCH_ENABLED #import diff --git a/ios/RNUnityView.mm b/ios/RNUnityView.mm index 6406f64..81471a1 100644 --- a/ios/RNUnityView.mm +++ b/ios/RNUnityView.mm @@ -32,6 +32,42 @@ return ufw; } +UIWindow* RNUnityHostWindow(UIView* view, UIWindow* exclude) { + if (view.window != nil && view.window != exclude) { + return view.window; + } + + if (@available(iOS 13.0, *)) { + for (UIScene *scene in [[UIApplication sharedApplication] connectedScenes]) { + if (scene.activationState != UISceneActivationStateForegroundActive || + ![scene isKindOfClass:[UIWindowScene class]]) { + continue; + } + + id sceneDelegate = scene.delegate; + if ([sceneDelegate respondsToSelector:@selector(window)]) { + UIWindow *window = [(id)sceneDelegate window]; + if (window != nil && window != exclude) { + return window; + } + } + + for (UIWindow *window in ((UIWindowScene *)scene).windows) { + if (window.isKeyWindow && window != exclude) { + return window; + } + } + } + } + + id appDelegate = [[UIApplication sharedApplication] delegate]; + if ([appDelegate respondsToSelector:@selector(window)] && appDelegate.window != exclude) { + return appDelegate.window; + } + + return nil; +} + @implementation RNUnityView { // YES once the Fabric view paused Unity for recycling, so we know to resume // the shared instance when the view comes back on screen (#180). @@ -134,13 +170,15 @@ - (void)resumeUnity { - (void)unloadUnity { _unloaded = YES; _pausedForRecycle = NO; - UIWindow * main = [[[UIApplication sharedApplication] delegate] window]; + UIWindow * unityWindow = [self unityIsInitialized] ? [[[self ufw] appController] window] : nil; + UIWindow * main = RNUnityHostWindow(self, unityWindow); if(main != nil) { [main makeKeyAndVisible]; + } - if([self unityIsInitialized]) { - [[self ufw] unloadApplication]; - } + // Unload even without a host window: skipping it silently leaves the engine resident. + if([self unityIsInitialized]) { + [[self ufw] unloadApplication]; } } diff --git a/ios/RNUnityViewManager.mm b/ios/RNUnityViewManager.mm index 4c56a29..defc7cc 100644 --- a/ios/RNUnityViewManager.mm +++ b/ios/RNUnityViewManager.mm @@ -17,7 +17,7 @@ @implementation RNUnityViewManager - (UIView *)view { unity = [[RNUnityView alloc] init]; - UIWindow * main = [[[UIApplication sharedApplication] delegate] window]; + UIWindow * main = RNUnityHostWindow(nil, nil); if(main != nil) { [main makeKeyAndVisible]; From 7f684733ccee941563eda25b79ac3423ff1be7dc Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sat, 10 Oct 2026 00:48:26 +0300 Subject: [PATCH 2/2] fix(ios): restart Unity on remount after unload; accept inactive scenes calls unloadUnity on unmount. With the host-window fix the unload now really happens under the UIScene life cycle, but Fabric recycles the view and _unloaded stayed YES, so the next mount never restarted Unity. Clear it in prepareForRecycle. The host-window scene lookup also accepts foreground-inactive scenes. --- ios/RNUnityView.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ios/RNUnityView.mm b/ios/RNUnityView.mm index 81471a1..d66c417 100644 --- a/ios/RNUnityView.mm +++ b/ios/RNUnityView.mm @@ -39,7 +39,9 @@ if (@available(iOS 13.0, *)) { for (UIScene *scene in [[UIApplication sharedApplication] connectedScenes]) { - if (scene.activationState != UISceneActivationStateForegroundActive || + // Foreground-inactive still counts: unload can arrive while a system overlay is up. + if (scene.activationState == UISceneActivationStateUnattached || + scene.activationState == UISceneActivationStateBackground || ![scene isKindOfClass:[UIWindowScene class]]) { continue; } @@ -239,6 +241,10 @@ - (void)prepareForRecycle { [[self ufw] pause:true]; _pausedForRecycle = YES; } + + // calls unloadUnity on unmount. The recycled instance serves the next + // mount, so clear the teardown latch or layoutSubviews never starts Unity again. + _unloaded = NO; } + (ComponentDescriptorProvider)componentDescriptorProvider {