Skip to content

Commit 128773d

Browse files
committed
Sink event and interactions in nested stack
1 parent 552d470 commit 128773d

File tree

4 files changed

+61
-15
lines changed

4 files changed

+61
-15
lines changed

ios/RNSScreenStack.mm

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#import "RNSScreenWindowTraits.h"
3030
#import "RNSScrollViewFinder.h"
3131
#import "RNSTabsScreenViewController.h"
32+
#import "RNSViewInteractionAware.h"
3233
#import "UIScrollView+RNScreens.h"
3334
#import "UIView+RNSUtility.h"
3435
#import "integrations/RNSDismissibleModalProtocol.h"
@@ -47,7 +48,8 @@ @interface RNSScreenStackView () <
4748
UINavigationControllerDelegate,
4849
UIAdaptivePresentationControllerDelegate,
4950
UIGestureRecognizerDelegate,
50-
UIViewControllerTransitioningDelegate
51+
UIViewControllerTransitioningDelegate,
52+
RNSViewInteractionAware
5153
#ifdef RCT_NEW_ARCH_ENABLED
5254
,
5355
RCTMountingTransactionObserving
@@ -259,6 +261,7 @@ @implementation RNSScreenStackView {
259261
RNSPercentDrivenInteractiveTransition *_interactionController;
260262
__weak RNSScreenStackManager *_manager;
261263
BOOL _updateScheduled;
264+
UIPanGestureRecognizer *_sinkEventsPanGestureRecognizer;
262265
#ifdef RCT_NEW_ARCH_ENABLED
263266
/// Screens that are subject of `ShadowViewMutation::Type::Delete` mutation
264267
/// in current transaction. This vector should be populated when we receive notification via
@@ -304,6 +307,7 @@ - (void)initCommonProps
304307
_presentedModals = [NSMutableArray new];
305308
_controller = [RNSNavigationController new];
306309
_controller.delegate = self;
310+
_sinkEventsPanGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
307311
#if !TARGET_OS_TV && !TARGET_OS_VISION
308312
[self setupGestureHandlers];
309313
#endif
@@ -899,6 +903,20 @@ - (void)cancelTouchesInParent
899903
[[self rnscreens_findTouchHandlerInAncestorChain] rnscreens_cancelTouches];
900904
}
901905

906+
- (void)disableInteractions
907+
{
908+
// When transitioning between screens, disable interactions on stack subview which wraps the screens
909+
// and sink all gesture events. This should work for nested stacks and stack inside bottom tabs, inside stack.
910+
self.subviews[0].userInteractionEnabled = NO;
911+
[self addGestureRecognizer:_sinkEventsPanGestureRecognizer];
912+
}
913+
914+
- (void)enableInteractions
915+
{
916+
self.subviews[0].userInteractionEnabled = YES;
917+
[self removeGestureRecognizer:_sinkEventsPanGestureRecognizer];
918+
}
919+
902920
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
903921
{
904922
if (_disableSwipeBack) {
@@ -1265,6 +1283,15 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
12651283
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
12661284
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
12671285
{
1286+
if (otherGestureRecognizer == _sinkEventsPanGestureRecognizer) {
1287+
// When transition happens between two stack screens, a special "sink" recognizer is added, and then removed.
1288+
// It captures all gestures for the time of transition and does nothing, so that in nested stack scenario,
1289+
// the outer most stack does not recognize swipe gestures, otherwise it would dismiss the whole nested stack.
1290+
// For the recognizer to work as described, it should have precedence over all other recognizers.
1291+
// see also: enableInteractions, disableInteractions
1292+
return YES;
1293+
}
1294+
12681295
if (@available(iOS 26, *)) {
12691296
if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer &&
12701297
[self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
@protocol RNSViewInteractionAware
4+
5+
- (void)disableInteractions;
6+
7+
- (void)enableInteractions;
8+
9+
@end

ios/helpers/RNSViewInteractionManager.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
- (instancetype)init;
66

77
/**
8-
* Given a view, find a nearest parent on which it is safe to disable interactions, when transitioning between screens
9-
* in stack. When Stack is nested inside BottomTabs, it finds such view that the tabs are still interactive. Otherwise,
10-
* it will default to UIWindow. Makes sure that at most one view is disabled at any time, re-enabling interactions on
11-
* previously affected views.
8+
* Given a view, traverse its ancestors hierarchy and find a view that supports RNSViewInteractionAware protocol
9+
* and can disable interactions for the time of screen transition. Make sure that at most one view is disabled at any
10+
* time, re-enabling interactions on previously affected views when necessary.
1211
*/
1312
- (void)disableInteractionsForSubtreeWith:(UIView *)view;
1413

ios/helpers/RNSViewInteractionManager.mm

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import "RNSViewInteractionManager.h"
22
#import "RNSBottomTabsScreenComponentView.h"
3+
#import "RNSViewInteractionAware.h"
34

45
@implementation RNSViewInteractionManager {
56
__weak UIView *lastRootWithInteractionsDisabled;
@@ -13,28 +14,38 @@ - (instancetype)init
1314

1415
- (void)disableInteractionsForSubtreeWith:(UIView *)view
1516
{
16-
UIView *parent = view.superview;
17-
while (parent && ![parent isKindOfClass:UIWindow.class] &&
18-
![parent isKindOfClass:RNSBottomTabsScreenComponentView.class]) {
19-
parent = parent.superview;
17+
UIView *current = view;
18+
while (current && ![current isKindOfClass:UIWindow.class] &&
19+
![current respondsToSelector:@selector(disableInteractions)]) {
20+
current = current.superview;
2021
}
2122

22-
if (parent) {
23-
if (lastRootWithInteractionsDisabled && lastRootWithInteractionsDisabled != parent) {
23+
if (current) {
24+
if (lastRootWithInteractionsDisabled && lastRootWithInteractionsDisabled != current) {
2425
// When one view already has interactions disabled, and we request a different view,
2526
// we need to restore the first one
26-
lastRootWithInteractionsDisabled.userInteractionEnabled = YES;
27+
[self enableInteractionsForLastSubtree];
28+
}
29+
30+
if ([current respondsToSelector:@selector(disableInteractions)]) {
31+
[static_cast<id<RNSViewInteractionAware>>(current) disableInteractions];
32+
} else {
33+
current.userInteractionEnabled = NO;
2734
}
2835

29-
parent.userInteractionEnabled = NO;
30-
lastRootWithInteractionsDisabled = parent;
36+
lastRootWithInteractionsDisabled = current;
3137
}
3238
}
3339

3440
- (void)enableInteractionsForLastSubtree
3541
{
3642
if (lastRootWithInteractionsDisabled) {
37-
lastRootWithInteractionsDisabled.userInteractionEnabled = YES;
43+
if ([lastRootWithInteractionsDisabled respondsToSelector:@selector(enableInteractions)]) {
44+
[static_cast<id<RNSViewInteractionAware>>(lastRootWithInteractionsDisabled) enableInteractions];
45+
} else {
46+
lastRootWithInteractionsDisabled.userInteractionEnabled = YES;
47+
}
48+
3849
lastRootWithInteractionsDisabled = nil;
3950
}
4051
}

0 commit comments

Comments
 (0)