Skip to content

Commit ccd0ecb

Browse files
Merge branch 'wix:master' into master
2 parents 04ad933 + 0ab840a commit ccd0ecb

14 files changed

Lines changed: 216 additions & 7 deletions

File tree

android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/LayoutDirectionApplier.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ class LayoutDirectionApplier {
1010
val currentContext = root.view?.context ?: return
1111

1212
if (options.layout.direction.hasValue()) {
13-
root.activity.window.decorView.layoutDirection = options.layout.direction.get()
13+
val direction = options.layout.direction.get()
14+
root.activity?.window?.decorView?.let { decor ->
15+
decor.layoutDirection = direction
16+
}
1417
I18nUtil.instance.allowRTL(currentContext, options.layout.direction.isRtl)
1518
I18nUtil.instance.forceRTL(currentContext, options.layout.direction.isRtl)
1619
}

android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ private void applyNavigationBarVisibility(NavigationBarOptions options) {
137137
}
138138

139139
private void setNavigationBarBackgroundColor(NavigationBarOptions navigationBar) {
140+
if (activity == null) return;
140141
int defaultColor = SystemUiUtils.getDefaultNavBarColor();
141142
if (navigationBar.backgroundColor.canApplyValue()) {
142143
int color = navigationBar.backgroundColor.get(defaultColor);

ios/RNNComponentViewController.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ - (void)updateSearchResultsForSearchController:(UISearchController *)searchContr
155155
isFocused:searchController.searchBar.isFirstResponder];
156156
}
157157

158+
- (void)destroyReactView {
159+
[self.reactView removeFromSuperview];
160+
self.reactView = nil;
161+
}
162+
158163
- (void)screenPopped {
159164
[_eventEmitter sendScreenPoppedEvent:self.layoutInfo.componentId];
160165
}

ios/RNNStackController.mm

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,82 @@ - (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewControlle
4242

4343
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
4444
[self prepareForPop];
45-
return [super popViewControllerAnimated:animated];
45+
UIViewController *previousTop = self.topViewController;
46+
UIView *snapshot = [self snapshotTopView:animated];
47+
48+
UIViewController *poppedVC = [super popViewControllerAnimated:animated];
49+
if (!poppedVC) {
50+
[snapshot removeFromSuperview];
51+
return nil;
52+
}
53+
54+
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
55+
if (coordinator && coordinator.isInteractive) {
56+
// Interactive pop (swipe-back): remove snapshot overlay — UIKit shows the live
57+
// view during the gesture. Skip early teardown so the React view stays alive
58+
// if the gesture is cancelled. The delegate's didShowViewController handles
59+
// cleanup once the animation finishes.
60+
[snapshot removeFromSuperview];
61+
} else {
62+
[self teardownPoppedControllers:@[ poppedVC ] previousTop:previousTop];
63+
}
64+
return poppedVC;
65+
}
66+
67+
- (NSArray<UIViewController *> *)popToViewController:(UIViewController *)viewController
68+
animated:(BOOL)animated {
69+
UIViewController *previousTop = self.topViewController;
70+
UIView *snapshot = [self snapshotTopView:animated];
71+
72+
NSArray<UIViewController *> *poppedVCs =
73+
[super popToViewController:viewController animated:animated];
74+
if (poppedVCs.count > 0) {
75+
[self teardownPoppedControllers:poppedVCs previousTop:previousTop];
76+
} else {
77+
[snapshot removeFromSuperview];
78+
}
79+
return poppedVCs;
80+
}
81+
82+
- (NSArray<UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated {
83+
UIViewController *previousTop = self.topViewController;
84+
UIView *snapshot = [self snapshotTopView:animated];
85+
86+
NSArray<UIViewController *> *poppedVCs =
87+
[super popToRootViewControllerAnimated:animated];
88+
if (poppedVCs.count > 0) {
89+
[self teardownPoppedControllers:poppedVCs previousTop:previousTop];
90+
} else {
91+
[snapshot removeFromSuperview];
92+
}
93+
return poppedVCs;
94+
}
95+
96+
#pragma mark - React view teardown
97+
98+
- (UIView *)snapshotTopView:(BOOL)animated {
99+
if (!animated) return nil;
100+
UIViewController *topVC = self.topViewController;
101+
if (!topVC.isViewLoaded || !topVC.view.window) return nil;
102+
103+
UIView *snapshot = [topVC.view snapshotViewAfterScreenUpdates:NO];
104+
if (snapshot) {
105+
snapshot.frame = topVC.view.bounds;
106+
snapshot.autoresizingMask =
107+
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
108+
[topVC.view addSubview:snapshot];
109+
}
110+
return snapshot;
111+
}
112+
113+
- (void)teardownPoppedControllers:(NSArray<UIViewController *> *)poppedVCs
114+
previousTop:(UIViewController *)previousTop {
115+
for (UIViewController *vc in poppedVCs) {
116+
if (vc == previousTop && [vc isKindOfClass:[RNNComponentViewController class]]) {
117+
[[(RNNComponentViewController *)vc reactView] componentDidDisappear];
118+
}
119+
[vc destroyReactView];
120+
}
46121
}
47122

48123
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

ios/StackControllerDelegate.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ - (void)navigationController:(UINavigationController *)navigationController
5656
if (_presentedViewController &&
5757
![navigationController.viewControllers containsObject:_presentedViewController]) {
5858
[_presentedViewController screenPopped];
59+
[_presentedViewController destroyReactView];
5960
_isPopping = NO;
6061
}
6162

ios/TopBarPresenter.mm

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,21 @@ - (void)setBackButtonOptions:(RNNBackButtonOptions *)backButtonOptions {
160160
NSNumber *fontSize = [backButtonOptions.fontSize withDefault:nil];
161161

162162
UIViewController *previousViewControllerInStack = self.previousViewControllerInStack;
163-
UIBarButtonItem *backItem = [[RNNUIBarBackButtonItem alloc] initWithOptions:backButtonOptions];
164163
UINavigationItem *previousNavigationItem = previousViewControllerInStack.navigationItem;
165164

165+
BOOL hasCustomization = icon || color || title || fontFamily || fontSize ||
166+
backButtonOptions.displayMode.hasValue ||
167+
backButtonOptions.sfSymbol.hasValue ||
168+
backButtonOptions.iconBackground.hasValue ||
169+
backButtonOptions.enableMenu.hasValue ||
170+
![backButtonOptions.showTitle withDefault:YES];
171+
172+
if (!hasCustomization) {
173+
return;
174+
}
175+
176+
UIBarButtonItem *backItem = [[RNNUIBarBackButtonItem alloc] initWithOptions:backButtonOptions];
177+
166178
if (@available(iOS 13.0, *)) {
167179
UIImage *sfSymbol = [UIImage systemImageNamed:[backButtonOptions.sfSymbol withDefault:nil]];
168180
if (backButtonOptions.sfSymbol.hasValue) {
@@ -195,7 +207,9 @@ - (void)setBackButtonOptions:(RNNBackButtonOptions *)backButtonOptions {
195207
cornerRadius:cornerRadius];
196208
}
197209

198-
[self setBackIndicatorImage:icon withColor:color];
210+
if (icon) {
211+
[self setBackIndicatorImage:icon withColor:color];
212+
}
199213

200214
title = title ? title : (previousNavigationItem.title ? previousNavigationItem.title : @"");
201215

ios/UIViewController+LayoutProtocol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ typedef void (^RNNReactViewReadyCompletionBlock)(void);
1818

1919
- (void)destroy;
2020

21+
- (void)destroyReactView;
22+
2123
- (void)mergeOptions:(RNNNavigationOptions *)options;
2224

2325
- (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child;

playground/e2e/Stack.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,15 @@ describe('Stack', () => {
183183
await expect(elementByLabel('pop promise resolved with: ChildId')).toBeVisible();
184184
});
185185

186+
it.e2e('pop and re-push same component should not have stale unmount', async () => {
187+
await elementById(TestIDs.PUSH_UNMOUNT_RACE_BTN).tap();
188+
await sleep(800);
189+
await expect(elementByLabel('loaded')).toBeVisible();
190+
await elementById(TestIDs.POP_AND_REPUSH_BTN).tap();
191+
await sleep(1000);
192+
await expect(elementByLabel('loaded')).toBeVisible();
193+
});
194+
186195
it('pop from root screen should do nothing', async () => {
187196
await elementById(TestIDs.POP_BTN).tap();
188197
await expect(elementById(TestIDs.STACK_SCREEN_HEADER)).toBeVisible();

playground/ios/NavigationTests/RNNStackPresenterTest.mm

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@ - (void)testApplyOptions_shouldSetBackButtonOnBoundViewController_withHideTitle
7373

7474
- (void)testApplyOptions_shouldSetBackButtonOnBoundViewController_withDefaultValues {
7575
[self.uut applyOptions:self.options];
76-
XCTAssertTrue(
77-
[self.boundViewController.viewControllers.firstObject.navigationItem.backBarButtonItem.title
78-
isEqualToString:@""]);
76+
XCTAssertNil(
77+
self.boundViewController.viewControllers.firstObject.navigationItem.backBarButtonItem);
7978
}
8079

8180
- (void)testSetBackButtonIcon_withColor_shouldSetColor {

playground/src/screens/Screens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ const Screens = {
122122
},
123123
SystemUiOptions: SystemUiOptions,
124124
StatusBarFirstTab,
125+
UnmountRace: 'UnmountRace',
125126
KeyboardScreen: 'KeyboardScreen',
126127
TopBarBackground: 'TopBarBackground',
127128
Toast: 'Toast',

0 commit comments

Comments
 (0)