From 58a1c0bba933e3dd37813db7713b56d249608827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Mon, 20 Apr 2026 02:11:00 +0000 Subject: [PATCH] fix(ios): resolve pauseUnity, memory leaks, and Fabric event issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix pauseUnity parameter type from BOOL* (pointer) to BOOL — the pointer was always non-nil, so Unity always paused and never resumed. Fix memory leak in initUnityModule where strdup'd strings and malloc'd array were never freed. Pass dictionary (not nil) to onPlayerUnload/onPlayerQuit callbacks to prevent potential crashes. Add unregisterAPIforNativeCalls to clear NativeCallProxy's strong reference on unload. Guard sendMessageToMobileApp against nil api. Fix ViewManager using file-global unity variable instead of the resolved view from viewRegistry. Fix Fabric prepareForRecycle destroying the singleton Unity runtime. Wire up onPlayerUnload and onPlayerQuit event emitters for Fabric (New Arch). Fix Fabric onUnityMessage lambda to use weak self instead of capturing raw C++ pointer. Respect androidKeepPlayerMounted in componentWillUnmount. Co-Authored-By: Claude Opus 4.6 --- .../Assets/Plugins/iOS/NativeCallProxy.h | 1 + .../Assets/Plugins/iOS/NativeCallProxy.mm | 9 ++- ios/RNUnityView.h | 4 +- ios/RNUnityView.mm | 59 ++++++++++++++----- ios/RNUnityViewManager.mm | 14 ++--- src/UnityView.tsx | 2 +- unity/Assets/Plugins/iOS/NativeCallProxy.h | 1 + unity/Assets/Plugins/iOS/NativeCallProxy.mm | 9 ++- 8 files changed, 71 insertions(+), 28 deletions(-) diff --git a/example/unity/Assets/Plugins/iOS/NativeCallProxy.h b/example/unity/Assets/Plugins/iOS/NativeCallProxy.h index aecfee6..18bc274 100644 --- a/example/unity/Assets/Plugins/iOS/NativeCallProxy.h +++ b/example/unity/Assets/Plugins/iOS/NativeCallProxy.h @@ -11,5 +11,6 @@ __attribute__ ((visibility("default"))) @interface FrameworkLibAPI : NSObject +(void) registerAPIforNativeCalls:(id) aApi; ++(void) unregisterAPIforNativeCalls; @end diff --git a/example/unity/Assets/Plugins/iOS/NativeCallProxy.mm b/example/unity/Assets/Plugins/iOS/NativeCallProxy.mm index 55661e7..38ce685 100644 --- a/example/unity/Assets/Plugins/iOS/NativeCallProxy.mm +++ b/example/unity/Assets/Plugins/iOS/NativeCallProxy.mm @@ -9,12 +9,19 @@ +(void) registerAPIforNativeCalls:(id) aApi api = aApi; } ++(void) unregisterAPIforNativeCalls +{ + api = NULL; +} + @end extern "C" { void sendMessageToMobileApp(const char* message) { - return [api sendMessageToMobileApp:[NSString stringWithUTF8String:message]]; + if (api != NULL) { + [api sendMessageToMobileApp:[NSString stringWithUTF8String:message]]; + } } } diff --git a/ios/RNUnityView.h b/ios/RNUnityView.h index 23824e4..622adb2 100644 --- a/ios/RNUnityView.h +++ b/ios/RNUnityView.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) RCTBubblingEventBlock _Nullable onPlayerQuit; - (void)unloadUnity; -- (void)pauseUnity:(BOOL * _Nonnull)pause; +- (void)pauseUnity:(BOOL)pause; - (void)postMessage:(NSString* _Nonnull )gameObject methodName:(NSString* _Nonnull)methodName message:(NSString* _Nonnull) message; @end @@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_END @property (nonatomic, copy) RCTBubblingEventBlock _Nullable onPlayerQuit; - (void)unloadUnity; -- (void)pauseUnity:(BOOL * _Nonnull)pause; +- (void)pauseUnity:(BOOL)pause; - (void)postMessage:(NSString* _Nonnull )gameObject methodName:(NSString* _Nonnull)methodName message:(NSString* _Nonnull) message; diff --git a/ios/RNUnityView.mm b/ios/RNUnityView.mm index a4caa74..cee3e69 100644 --- a/ios/RNUnityView.mm +++ b/ios/RNUnityView.mm @@ -61,6 +61,14 @@ - (void)initUnityModule { array[count] = NULL; [[self ufw] runEmbeddedWithArgc: gArgc argv: array appLaunchOpts: appLaunchOpts]; + + // Free the strdup'd strings and the malloc'd array + for (unsigned i = 0; i < count; i++) + { + free(array[i]); + } + free(array); + [[self ufw] appController].quitHandler = ^(){ NSLog(@"AppController.quitHandler called"); }; [self.ufw.appController.rootView removeFromSuperview]; @@ -90,7 +98,7 @@ - (void)layoutSubviews { } } -- (void)pauseUnity:(BOOL * _Nonnull)pause { +- (void)pauseUnity:(BOOL)pause { if([self unityIsInitialized]) { [[self ufw] pause:pause]; } @@ -102,6 +110,7 @@ - (void)unloadUnity { [main makeKeyAndVisible]; if([self unityIsInitialized]) { + [NSClassFromString(@"FrameworkLibAPI") unregisterAPIforNativeCalls]; [[self ufw] unloadApplication]; } } @@ -123,7 +132,7 @@ - (void)unityDidUnload:(NSNotification*)notification { [self setUfw: nil]; if (self.onPlayerUnload) { - self.onPlayerUnload(nil); + self.onPlayerUnload(@{@"message": @"unloaded"}); } } } @@ -134,7 +143,7 @@ - (void)unityDidQuit:(NSNotification*)notification { [self setUfw: nil]; if (self.onPlayerQuit) { - self.onPlayerQuit(nil); + self.onPlayerQuit(@{@"message": @"quit"}); } } } @@ -157,15 +166,11 @@ - (void)postMessage:(NSString *)gameObject methodName:(NSString*)methodName mess - (void)prepareForRecycle { [super prepareForRecycle]; - if ([self unityIsInitialized]) { - [[self ufw] unloadApplication]; - - NSArray *viewsToRemove = self.subviews; - for (UIView *v in viewsToRemove) { - [v removeFromSuperview]; - } - - [self setUfw:nil]; + // Only detach the Unity view from this component — do NOT unload the + // singleton Unity runtime, as that would break it for any future views. + NSArray *viewsToRemove = self.subviews; + for (UIView *v in viewsToRemove) { + [v removeFromSuperview]; } } @@ -178,15 +183,39 @@ - (instancetype)initWithFrame:(CGRect)frame { static const auto defaultProps = std::make_shared(); _props = defaultProps; - self.onUnityMessage = [self](NSDictionary* data) { - if (_eventEmitter != nil) { - auto gridViewEventEmitter = std::static_pointer_cast(_eventEmitter); + __weak RNUnityView *weakSelf = self; + self.onUnityMessage = ^(NSDictionary* data) { + RNUnityView *strongSelf = weakSelf; + if (strongSelf && strongSelf->_eventEmitter != nil) { + auto gridViewEventEmitter = std::static_pointer_cast(strongSelf->_eventEmitter); facebook::react::RNUnityViewEventEmitter::OnUnityMessage event = { .message=[[data valueForKey:@"message"] UTF8String] }; gridViewEventEmitter->onUnityMessage(event); } }; + + self.onPlayerUnload = ^(NSDictionary* data) { + RNUnityView *strongSelf = weakSelf; + if (strongSelf && strongSelf->_eventEmitter != nil) { + auto eventEmitter = std::static_pointer_cast(strongSelf->_eventEmitter); + facebook::react::RNUnityViewEventEmitter::OnPlayerUnload event = { + .message=[[data valueForKey:@"message"] UTF8String] + }; + eventEmitter->onPlayerUnload(event); + } + }; + + self.onPlayerQuit = ^(NSDictionary* data) { + RNUnityView *strongSelf = weakSelf; + if (strongSelf && strongSelf->_eventEmitter != nil) { + auto eventEmitter = std::static_pointer_cast(strongSelf->_eventEmitter); + facebook::react::RNUnityViewEventEmitter::OnPlayerQuit event = { + .message=[[data valueForKey:@"message"] UTF8String] + }; + eventEmitter->onPlayerQuit(event); + } + }; } return self; diff --git a/ios/RNUnityViewManager.mm b/ios/RNUnityViewManager.mm index f9b79bd..76db227 100644 --- a/ios/RNUnityViewManager.mm +++ b/ios/RNUnityViewManager.mm @@ -13,10 +13,8 @@ @implementation RNUnityViewManager RCT_EXPORT_VIEW_PROPERTY(onPlayerUnload, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPlayerQuit, RCTBubblingEventBlock) -RNUnityView *unity; - - (UIView *)view { - unity = [[RNUnityView alloc] init]; + RNUnityView *unity = [[RNUnityView alloc] init]; UIWindow * main = [[[UIApplication sharedApplication] delegate] window]; if(main != nil) { @@ -41,18 +39,18 @@ + (BOOL)requiresMainQueueSetup { RCTLogError(@"Cannot find NativeView with tag #%@", reactTag); return; } - [unity postMessage:(NSString *)gameObject methodName:(NSString *)methodName message:(NSString *)message]; + [view postMessage:(NSString *)gameObject methodName:(NSString *)methodName message:(NSString *)message]; }]; } -RCT_EXPORT_METHOD(pauseUnity:(nonnull NSNumber*) reactTag pause:(BOOL * _Nonnull)pause) { +RCT_EXPORT_METHOD(pauseUnity:(nonnull NSNumber*) reactTag pause:(BOOL)pause) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RNUnityView *view = (RNUnityView*) viewRegistry[reactTag]; if (!view || ![view isKindOfClass:[RNUnityView class]]) { RCTLogError(@"Cannot find NativeView with tag #%@", reactTag); return; } - [unity pauseUnity:(BOOL * _Nonnull)pause]; + [view pauseUnity:pause]; }]; } @@ -63,7 +61,7 @@ + (BOOL)requiresMainQueueSetup { RCTLogError(@"Cannot find NativeView with tag #%@", reactTag); return; } - [unity pauseUnity:(BOOL * _Nonnull)false]; + [view pauseUnity:NO]; }]; } @@ -74,7 +72,7 @@ + (BOOL)requiresMainQueueSetup { RCTLogError(@"Cannot find NativeView with tag #%@", reactTag); return; } - [unity unloadUnity]; + [view unloadUnity]; }]; } diff --git a/src/UnityView.tsx b/src/UnityView.tsx index 0b21cea..5b020da 100644 --- a/src/UnityView.tsx +++ b/src/UnityView.tsx @@ -66,7 +66,7 @@ export default class UnityView extends React.Component { } componentWillUnmount() { - if (this.ref.current) { + if (this.ref.current && !this.props.androidKeepPlayerMounted) { Commands.unloadUnity(this.ref.current); } } diff --git a/unity/Assets/Plugins/iOS/NativeCallProxy.h b/unity/Assets/Plugins/iOS/NativeCallProxy.h index aecfee6..18bc274 100644 --- a/unity/Assets/Plugins/iOS/NativeCallProxy.h +++ b/unity/Assets/Plugins/iOS/NativeCallProxy.h @@ -11,5 +11,6 @@ __attribute__ ((visibility("default"))) @interface FrameworkLibAPI : NSObject +(void) registerAPIforNativeCalls:(id) aApi; ++(void) unregisterAPIforNativeCalls; @end diff --git a/unity/Assets/Plugins/iOS/NativeCallProxy.mm b/unity/Assets/Plugins/iOS/NativeCallProxy.mm index 55661e7..38ce685 100644 --- a/unity/Assets/Plugins/iOS/NativeCallProxy.mm +++ b/unity/Assets/Plugins/iOS/NativeCallProxy.mm @@ -9,12 +9,19 @@ +(void) registerAPIforNativeCalls:(id) aApi api = aApi; } ++(void) unregisterAPIforNativeCalls +{ + api = NULL; +} + @end extern "C" { void sendMessageToMobileApp(const char* message) { - return [api sendMessageToMobileApp:[NSString stringWithUTF8String:message]]; + if (api != NULL) { + [api sendMessageToMobileApp:[NSString stringWithUTF8String:message]]; + } } }