diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index d8c2b13ee1..454d5cea22 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -104,7 +104,7 @@ namespace react = facebook::react; @property (nonatomic) BOOL hideKeyboardOnSwipe; @property (nonatomic) BOOL customAnimationOnSwipe; @property (nonatomic) BOOL preventNativeDismiss; -@property (nonatomic, retain) RNSScreen *controller; +@property (nonatomic, weak) RNSScreen *controller; @property (nonatomic, copy) NSDictionary *gestureResponseDistance; @property (nonatomic) int activityState; @property (nonatomic, nullable) NSString *screenId; @@ -213,6 +213,11 @@ namespace react = facebook::react; */ - (BOOL)registerContentWrapper:(nonnull RNSScreenContentWrapper *)contentWrapper contentHeightErrata:(float)errata; +/** + * Sets the retained controller to break the retain cycle. + */ +- (void)setRetainedController:(RNSScreen *_Nullable)controller; + @end @interface UIView (RNSScreen) diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index f21aa305eb..a8e240952e 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -53,6 +53,36 @@ float contentHeightErrata{0.f}; }; +@interface RNSWeakProxy : NSProxy + +@property (nonatomic, weak, readonly) id target; + +- (instancetype)initWithTarget:(id)target; + +@end + +@implementation RNSWeakProxy + +- (instancetype)initWithTarget:(id)target +{ + _target = target; + return self; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + return [_target methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + if (_target) { + [invocation invokeWithTarget:_target]; + } +} + +@end + @interface RNSScreenView () < UIAdaptivePresentationControllerDelegate, #if !TARGET_OS_TV @@ -69,6 +99,7 @@ @interface RNSScreenView () < @implementation RNSScreenView { __weak RNS_REACT_SCROLL_VIEW_COMPONENT *_sheetsScrollView; + RNSScreen *_retainedController; /// Up-to-date only when sheet is in `fitToContents` mode. CGFloat _sheetContentHeight; @@ -121,7 +152,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge - (void)initCommonProps { - _controller = [[RNSScreen alloc] initWithView:self]; + RNSScreen *controller = [[RNSScreen alloc] initWithView:self]; + _controller = controller; + _retainedController = controller; _stackPresentation = RNSScreenStackPresentationPush; _stackAnimation = RNSScreenStackAnimationDefault; _gestureEnabled = YES; @@ -950,9 +983,15 @@ - (BOOL)isTransparentModal - (void)invalidateImpl { _controller = nil; + _retainedController = nil; [_sheetsScrollView removeObserver:self forKeyPath:@"bounds" context:nil]; } +- (void)setRetainedController:(RNSScreen *)controller +{ + _retainedController = controller; +} + #ifndef RCT_NEW_ARCH_ENABLED - (void)invalidate { @@ -1861,6 +1900,9 @@ - (void)willMoveToParentViewController:(UIViewController *)parent _previousFirstResponder = responder; } } else { + // When moving to a parent controller, release the retained controller + // The parent will now hold a strong reference to this controller + [self.screenView setRetainedController:nil]; [self.screenView overrideScrollViewBehaviorInFirstDescendantChainIfNeeded]; [self.screenView updateContentScrollViewEdgeEffectsIfExists]; } @@ -1884,6 +1926,11 @@ - (id)findFirstResponder:(UIView *)parent - (void)setupProgressNotification { + // Clean up any existing animation timer before setting up a new one + // This prevents memory leaks when viewWillDisappear is called multiple times + [_animationTimer invalidate]; + _animationTimer = nil; + if (self.transitionCoordinator != nil) { if (!self.transitionCoordinator.isAnimated) { // If the transition is not animated, there is no point to set up animation @@ -1898,7 +1945,9 @@ - (void)setupProgressNotification auto animation = ^(id _Nonnull context) { [[context containerView] addSubview:self->_fakeView]; self->_fakeView.alpha = 1.0; - self->_animationTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleAnimation)]; + // Use weak proxy to prevent retain cycle between CADisplayLink and RNSScreen + RNSWeakProxy *weakProxy = [[RNSWeakProxy alloc] initWithTarget:self]; + self->_animationTimer = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(handleAnimation)]; [self->_animationTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; }; @@ -1907,6 +1956,7 @@ - (void)setupProgressNotification completion:^(id _Nonnull context) { [self->_animationTimer setPaused:YES]; [self->_animationTimer invalidate]; + self->_animationTimer = nil; [self->_fakeView removeFromSuperview]; }]; } @@ -2187,6 +2237,13 @@ - (void)setViewToSnapshot #endif // RCT_NEW_ARCH_ENABLED +- (void)dealloc +{ + // Clean up animation timer to prevent memory leaks + [_animationTimer invalidate]; + _animationTimer = nil; +} + @end @implementation RNSScreenManager