Skip to content

Commit a2526cd

Browse files
committed
Disable interactions on minimal required subroot
1 parent 3a588b1 commit a2526cd

File tree

5 files changed

+80
-5
lines changed

5 files changed

+80
-5
lines changed

ios/RNSScreen.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#import "RNSScreenContentWrapper.h"
88
#import "RNSScrollEdgeEffectApplicator.h"
99
#import "RNSScrollViewBehaviorOverriding.h"
10+
#import "RNSViewInteractionManager.h";
1011

1112
#if !TARGET_OS_TV
1213
#import "RNSOrientationProviding.h"
@@ -155,6 +156,8 @@ namespace react = facebook::react;
155156
- (void)notifyDismissedWithCount:(int)dismissCount;
156157
- (instancetype)initWithFrame:(CGRect)frame;
157158

159+
+ (RNSViewInteractionManager *)viewInteractionManagerInstance;
160+
158161
/**
159162
* Tell `Screen` that it will be unmounted in next transaction.
160163
* The component needs this so that we can later decide whether to

ios/RNSScreen.mm

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@ - (void)initCommonProps
144144
#endif // RCT_NEW_ARCH_ENABLED
145145
}
146146

147+
+ (RNSViewInteractionManager *)viewInteractionManagerInstance
148+
{
149+
static RNSViewInteractionManager *manager = nil;
150+
static dispatch_once_t onceToken;
151+
dispatch_once(&onceToken, ^{
152+
manager = [[RNSViewInteractionManager alloc] init];
153+
});
154+
155+
return manager;
156+
}
157+
147158
- (BOOL)getFullScreenSwipeShadowEnabled
148159
{
149160
if (@available(iOS 26, *)) {
@@ -795,15 +806,15 @@ - (void)willMoveToWindow:(UIWindow *)newWindow
795806
// Furthermore, a stack put inside a modal will exist in an entirely different hierarchy
796807
// To be sure, we block interactions on the whole window.
797808
// Note that newWindows is nil when moving from instead of moving to, and Obj-C handles nil correctly
798-
newWindow.userInteractionEnabled = false;
809+
[RNSScreenView.viewInteractionManagerInstance disableInteractionsForSubtreeWith:self];
799810
}
800811
}
801812

802813
- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
803814
{
804815
if (@available(iOS 26, *)) {
805816
// Disable interactions to disallow multiple modals dismissed at once; see willMoveToWindow
806-
presentationController.containerView.window.userInteractionEnabled = false;
817+
[RNSScreenView.viewInteractionManagerInstance disableInteractionsForSubtreeWith:self];
807818
}
808819

809820
#if !RCT_NEW_ARCH_ENABLED
@@ -834,7 +845,7 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)pr
834845
{
835846
if (@available(iOS 26, *)) {
836847
// Reenable interactions; see presentationControllerWillDismiss
837-
presentationController.containerView.window.userInteractionEnabled = true;
848+
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
838849
}
839850

840851
// NOTE(kkafar): We should consider depracating the use of gesture cancel here & align
@@ -850,7 +861,7 @@ - (void)presentationControllerDidDismiss:(UIPresentationController *)presentatio
850861
if (@available(iOS 26, *)) {
851862
// Reenable interactions; see presentationControllerWillDismiss
852863
// Dismissed screen doesn't hold a reference to window, but presentingViewController.view does
853-
presentationController.presentingViewController.view.window.userInteractionEnabled = true;
864+
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
854865
}
855866

856867
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
@@ -1654,7 +1665,7 @@ - (void)viewDidAppear:(BOOL)animated
16541665
{
16551666
if (@available(iOS 26, *)) {
16561667
// Reenable interactions, see willMoveToWindow
1657-
self.view.window.userInteractionEnabled = true;
1668+
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
16581669
}
16591670
[super viewDidAppear:animated];
16601671
if (!_isSwiping || _shouldNotify) {
@@ -1696,6 +1707,10 @@ - (void)viewDidDisappear:(BOOL)animated
16961707
#else
16971708
[self traverseForScrollView:self.screenView];
16981709
#endif
1710+
if (@available(iOS 26, *)) {
1711+
// Reenable interactions, see willMoveToWindow
1712+
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
1713+
}
16991714
}
17001715

17011716
- (void)viewDidLayoutSubviews

ios/RNSScreenStack.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,12 @@ - (void)didMoveToWindow
412412
[self maybeAddToParentAndUpdateContainer];
413413
}
414414
#endif
415+
if (self.window == nil) {
416+
// When hot reload happens that would remove the whole stack, disabling the interaction on a screen out transition
417+
// will not be matched with enabling the interactions on another screen's in transition. We need to make sure
418+
// that the subtree is interactive again
419+
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
420+
}
415421
}
416422

417423
- (void)maybeAddToParentAndUpdateContainer
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#pragma once
2+
3+
@interface RNSViewInteractionManager : NSObject
4+
5+
- (instancetype)init;
6+
7+
- (void)disableInteractionsForSubtreeWith:(UIView *)view;
8+
9+
- (void)enableInteractionsForLastSubtree;
10+
11+
@end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#import "RNSViewInteractionManager.h"
2+
#import "RNSBottomTabsScreenComponentView.h"
3+
4+
@implementation RNSViewInteractionManager {
5+
__weak UIView *lastRootWithInteractionsDisabled;
6+
}
7+
8+
- (instancetype)init
9+
{
10+
lastRootWithInteractionsDisabled = nil;
11+
return self;
12+
}
13+
14+
- (void)disableInteractionsForSubtreeWith:(UIView *)view
15+
{
16+
UIView *parent = view.superview;
17+
while (parent && ![parent isKindOfClass:UIWindow.class] &&
18+
![parent isKindOfClass:RNSBottomTabsScreenComponentView.class]) {
19+
parent = parent.superview;
20+
}
21+
22+
if (parent) {
23+
if (lastRootWithInteractionsDisabled && lastRootWithInteractionsDisabled != parent) {
24+
lastRootWithInteractionsDisabled.userInteractionEnabled = YES;
25+
}
26+
27+
parent.userInteractionEnabled = NO;
28+
lastRootWithInteractionsDisabled = parent;
29+
}
30+
}
31+
32+
- (void)enableInteractionsForLastSubtree
33+
{
34+
if (lastRootWithInteractionsDisabled) {
35+
lastRootWithInteractionsDisabled.userInteractionEnabled = YES;
36+
lastRootWithInteractionsDisabled = nil;
37+
}
38+
}
39+
40+
@end

0 commit comments

Comments
 (0)