Skip to content

Commit 610a4df

Browse files
committed
Prevent navigation to "preventNativeSelection enabled" tabs that are on
stack of more navigation controller In such cases, now we emit an `OnTabSelectionPrevented` event. I've thought about whether we should, but I came to conclusion that it is better to expose this information and let user ignore it, than not expose it at all, in the end we're effectively preventing user navigation to such tab. In the future we might consider adding some payload to indicate that the prevention happened exactly in such case, so that it is easier to ignore.
1 parent 80ffaa6 commit 610a4df

1 file changed

Lines changed: 58 additions & 9 deletions

File tree

ios/tabs/host/RNSTabBarController.mm

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ - (void)userDidSelectViewController:(nonnull UIViewController *)viewController
278278
[self updateNavigationStateOnModelUpdate];
279279

280280
if ([self isSelectedViewControllerTheMoreNavigationController]) {
281+
[self disableNavigationBarInMoreNavigationController];
281282
[self setupMoreNavigationControllerDelegateIfNeeded];
282283
}
283284

@@ -348,9 +349,23 @@ - (BOOL)tabBarController:(UITabBarController *)tabBarController
348349
if (shouldPreventTabSelection) {
349350
// Ideally we'd call this AFTER we prevent, but there is no appropriate callback.
350351
[self onDidPreventUserFromSelectingViewControllerWithKey:[self screenKeyForViewController:viewController]];
352+
return NO;
351353
}
352354

353-
return !shouldPreventTabSelection;
355+
// If we're gonna allow navigation to `moreNavigationController`, then we need to ensure
356+
// that on top of its stack there is no controller with preventNativeSelection enabled.
357+
// In such case, we want to pop to root.
358+
// We do it here, because in `tabBarController:didSelectViewController:` we won't receive
359+
// `moreNavigationController` in case there is already a tab pushed on the stack.
360+
if ([self isViewControllerTheMoreNavigationController:viewController]) {
361+
auto *poppedViewController = [self popToRootInMoreNavigationControllerRespectSelectionPrevention:YES animated:NO];
362+
if (poppedViewController != nil) {
363+
// We actually popped something -> let's notify JS realm of this fact.
364+
[self onDidPreventUserFromSelectingViewControllerWithKey:[self screenKeyForViewController:poppedViewController]];
365+
}
366+
}
367+
368+
return YES;
354369
}
355370

356371
- (void)tabBarController:(UITabBarController *)tabBarController
@@ -366,10 +381,6 @@ - (void)tabBarController:(UITabBarController *)tabBarController
366381
@"[RNScreens] Unexpected type of controller: %@",
367382
viewController.class);
368383

369-
if (isNextViewControllerMoreNavigationController) {
370-
[self disableNavigationBarInMoreNavigationController];
371-
}
372-
373384
[self userDidSelectViewController:viewController];
374385
}
375386

@@ -513,7 +524,7 @@ - (void)updateSelectedViewControllerInner
513524

514525
// Animate only if we're currently in context of more view controller.
515526
BOOL shouldAnimate = [self isMoreNavigationControllerTabBarItemSelected];
516-
[self popToRootInMoreNavigationControllerIfNeededAnimated:shouldAnimate];
527+
[self popToRootInMoreNavigationControllerRespectSelectionPrevention:NO animated:shouldAnimate];
517528

518529
// Also disable the header - we don't control it, but it impacts the layout
519530
// in ways Yoga is not aware of. The simplest option here is to disable it.
@@ -722,18 +733,56 @@ - (BOOL)isMoreNavigationControllerTabBarItemSelected
722733
- (void)disableNavigationBarInMoreNavigationController
723734
{
724735
#if RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
725-
[self.moreNavigationController setNavigationBarHidden:YES animated:NO];
736+
if (!self.moreNavigationController.navigationBar.isHidden) {
737+
[self.moreNavigationController setNavigationBarHidden:YES animated:NO];
738+
}
726739
#endif // RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
727740
}
728741

729-
- (void)popToRootInMoreNavigationControllerIfNeededAnimated:(BOOL)isAnimated
742+
- (nullable UIViewController *)popToRootInMoreNavigationControllerRespectSelectionPrevention:
743+
(BOOL)shouldRespectSelectionPrevention
744+
animated:(BOOL)shouldAnimate
730745
{
731746
#if RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
732747
if ([self isMoreNavigationControllerPresentInTabBar] && self.moreNavigationController.viewControllers.count > 1) {
733748
// We quietly assume here, that the root view controller is the `UIMoreListViewController`.
734-
[self.moreNavigationController popToRootViewControllerAnimated:isAnimated];
749+
if (shouldRespectSelectionPrevention) {
750+
UIViewController *topViewController = self.moreNavigationController.topViewController;
751+
RCTAssert(
752+
[topViewController isKindOfClass:RNSTabsScreenViewController.class],
753+
@"[RNScreens] Unexpected type of view controller on moreNavigationControllerStack: %@",
754+
topViewController.class);
755+
RNSTabsScreenViewController *screenController = static_cast<RNSTabsScreenViewController *>(topViewController);
756+
if (screenController.isPreventNativeSelectionEnabled) {
757+
return [self popToRootMoreNavigationController:self.moreNavigationController animated:shouldAnimate];
758+
}
759+
} else {
760+
return [self popToRootMoreNavigationController:self.moreNavigationController animated:shouldAnimate];
761+
}
735762
}
736763
#endif // RNS_MORE_NAVIGATION_CONTROLLER_AVAILABLE
764+
return nil;
765+
}
766+
767+
/**
768+
* Pops the top view controller from more navigation controller. We expect at most two controllers on the stack of more
769+
* navigation controller. If this assumption ever becomes invalid, this method needs to be updated.
770+
*
771+
* @returns nil if there was nothing to pop, the topViewController otherwise.
772+
*/
773+
- (nullable UIViewController *)popToRootMoreNavigationController:
774+
(nonnull UINavigationController *)moreNavigationController
775+
animated:(BOOL)animated
776+
{
777+
if (moreNavigationController.viewControllers.count < 2) {
778+
return nil;
779+
}
780+
781+
auto *poppedViewControllers = [moreNavigationController popToRootViewControllerAnimated:animated];
782+
RCTAssert(
783+
poppedViewControllers != nil && poppedViewControllers.count == 1,
784+
@"[RNScreens] Expected exactly one view controller to be popped");
785+
return [poppedViewControllers firstObject];
737786
}
738787

739788
- (void)setupMoreNavigationControllerDelegateIfNeeded

0 commit comments

Comments
 (0)