Skip to content

Commit 030cb44

Browse files
authored
fix(iOS, Stack v4): prevent header subview memory leak (#3653)
## Description Prevents header subview memory leak. Closes software-mansion/react-native-screens-labs#696. ### Details When header items were introduced, potential memory leak was spotted: #2987 (comment). From my testing, it seems that after introducing a wrapper view on iOS 26+ (#3449), UIKit manages to deallocate all necessary views. On iOS 18 however I managed to create a memory leak (see video "before"). There is one thing that confuses me a little bit - it seems like on iOS 26, UIKit retains the reference to bar button item & wrapper view a little bit longer than expected (e.g. via `_UINavigationBarContentView`). When we push a screen with bar button item, pop it, it seems like the reference remains but when we push a new screen, it is deallocated. Also, when first screen uses `headerShown: false`, the behavior seems similar also on iOS 18 but this time with header subview reference. It is also deallocated after pushing another screen. I don't think that we need to worry about this for now but I wanted to point that out in PR description. ### Solution I decided to keep the reference to `UIBarButtonItem` inside `RNSScreenStackHeaderSubview` to maintain prop update logic (#2987 (comment)). I don't think that we can make it a weak reference because in header config we clear left/rightBarButtonItems from navigation item - I guess that the button might be deallocated before we attempt to re-use it (correct me if I'm wrong). Because of the above, I needed a place to clear the reference. I thought about willMoveToWindow/Superview callbacks but: 1. `willMoveToWindow` is called multiple times so it's not reliable 2. `willMoveToSuperview` is better but when we push another screen over a screen with bar button item, we will move to nil. I think that we don't want to clear the bar button then to maintain any (internal) state inside the button. At first, I wanted to use already existing logic in header config's `unmountChildComponentView` but this callback's Paper counterpart isn't called when entire screen is popped instead of only subview. That's why I used `invalidate` callback. On Paper, it's available via `RCTInvalidating`. On Fabric, this is available on RCTViewComponentView starting from RN 0.82. As next release will require RN 0.82 for Fabric, we can comfortably use this callback. ## Changes - add method to clear bar button reference - use it in `invalidate` callback ## Before & after - visual documentation ### Before https://github.com/user-attachments/assets/fb07fd55-8462-4fec-9791-85733c78e606 ### After https://github.com/user-attachments/assets/42ab5c6c-4a97-4db1-a098-60d08d39adc8 ## Test plan Run `Test3422`. Remove `headerShown: false` from `Screen1`. Push a screen and then pop it. Check memory graph for `HeaderSubview`. You can also override `dealloc` for header subview. ## Checklist - [x] Included code example that can be used to test this change. - [ ] Ensured that CI passes
1 parent bdef20a commit 030cb44

File tree

2 files changed

+22
-4
lines changed

2 files changed

+22
-4
lines changed

ios/RNSScreenStackHeaderSubview.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
1414
#ifdef RCT_NEW_ARCH_ENABLED
1515
RCTViewComponentView
1616
#else
17-
UIView
17+
UIView <RCTInvalidating>
1818
#endif
1919

2020
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
@@ -49,6 +49,9 @@ NS_ASSUME_NONNULL_BEGIN
4949
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView withFrame:(CGRect)frame;
5050
#endif
5151

52+
/**
53+
* Returns UIBarButtonItem associated with this subview. If it doesn't exist, UIBarButtonItem is created.
54+
*/
5255
- (UIBarButtonItem *)getUIBarButtonItem;
5356

5457
@end

ios/RNSScreenStackHeaderSubview.mm

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ @implementation RNSScreenStackHeaderSubview {
2727
#if !RCT_NEW_ARCH_ENABLED && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
2828
CGSize _lastReactFrameSize;
2929
#endif // !RCT_NEW_ARCH_ENABLED && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
30-
// TODO: Refactor this, so that we don't keep reference here at all.
31-
// Currently this likely creates retain cycle between subview & the bar button item.
30+
// This is a strong reference to UIBarButtonItem which creates a retain cycle.
31+
// The cycle is cleared via `invalidateUIBarButtonItem` method, called by `invalidate` callback.
3232
UIBarButtonItem *_barButtonItem;
3333
BOOL _hidesSharedBackground;
3434
}
@@ -223,6 +223,12 @@ - (void)reactSetFrame:(CGRect)frame
223223

224224
#endif // RCT_NEW_ARCH_ENABLED
225225

226+
// Used by both Fabric & Paper
227+
- (void)invalidate
228+
{
229+
[self invalidateUIBarButtonItem];
230+
}
231+
226232
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
227233

228234
// Starting from iOS 26, to center left and right subviews inside liquid glass backdrop,
@@ -278,7 +284,7 @@ - (UIBarButtonItem *)getUIBarButtonItem
278284
// 2. Set content hugging priority for RNSScreenStackHeaderSubview.
279285
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
280286
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
281-
287+
282288
// 3. Set compression resistance to prevent UIKit from shrinking the subview below its intrinsic size.
283289
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
284290
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
@@ -295,6 +301,15 @@ - (UIBarButtonItem *)getUIBarButtonItem
295301
return _barButtonItem;
296302
}
297303

304+
- (void)invalidateUIBarButtonItem
305+
{
306+
if (_type != RNSScreenStackHeaderSubviewTypeLeft && _type != RNSScreenStackHeaderSubviewTypeRight) {
307+
return;
308+
}
309+
310+
_barButtonItem = nil;
311+
}
312+
298313
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
299314
- (CGSize)intrinsicContentSize
300315
{

0 commit comments

Comments
 (0)