Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions FabricExample/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ featureFlags.experiment.synchronousHeaderConfigUpdatesEnabled = false;
featureFlags.experiment.synchronousHeaderSubviewUpdatesEnabled = false;
featureFlags.experiment.androidResetScreenShadowStateOnOrientationChangeEnabled =
true;
featureFlags.experiment.iosPreventReattachmentOfDismissedScreens = true;
featureFlags.experiment.iosPreventReattachmentOfDismissedModals = true;
featureFlags.experiment.ios26AllowInteractionsDuringTransition = true;
featureFlags.stable.debugLogging = true;

export default App;
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,6 @@ class ScreenStackViewManager :
StackFinishTransitioningEvent.EVENT_NAME to mutableMapOf("registrationName" to "onFinishTransitioning"),
)

// iosPreventReattachmentOfDismissedScreens is not available on Android,
// however we must override the setter
override fun setIosPreventReattachmentOfDismissedScreens(
view: ScreenStack?,
value: Boolean,
) = Unit

// iosPreventReattachmentOfDismissedModals is not available on Android,
// however we must override the setter
override fun setIosPreventReattachmentOfDismissedModals(
view: ScreenStack?,
value: Boolean,
) = Unit

// nativeContainerBackgroundColor is iOS-only because the native view hierarchy
// differs between platforms. On Android, ScreenStack is used directly as the
// container, so `style.backgroundColor` achieves the same effect.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,6 @@ open class ScreenViewManager :
value: Boolean,
) = Unit

override fun setIos26AllowInteractionsDuringTransition(
view: Screen?,
value: Boolean,
) = Unit

// END mark: iOS-only

override fun setAndroidResetScreenShadowStateOnOrientationChangeEnabled(
Expand Down
7 changes: 0 additions & 7 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#import "RNSScreenContentWrapper.h"
#import "RNSScrollEdgeEffectApplicator.h"
#import "RNSScrollViewBehaviorOverriding.h"
#import "RNSViewInteractionManager.h"

#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
Expand Down Expand Up @@ -155,12 +154,6 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)isModal;
- (BOOL)isPresentedAsNativeModal;

/**
* Holds a shared instance to a service that finds the view that needs to have interactions disabled for stack to not
* have multiple screen transitions at once.
*/
+ (RNSViewInteractionManager *)viewInteractionManagerInstance;

/**
* Tell `Screen` component that it has been removed from react state and can safely cleanup
* any retained resources.
Expand Down
49 changes: 0 additions & 49 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,6 @@ - (void)initCommonProps
_markedForUnmountInCurrentTransaction = NO;
}

+ (RNSViewInteractionManager *)viewInteractionManagerInstance
{
static RNSViewInteractionManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[RNSViewInteractionManager alloc] init];
});

return manager;
}

- (BOOL)getFullScreenSwipeShadowEnabled
{
if (@available(iOS 26, *)) {
Expand Down Expand Up @@ -648,22 +637,6 @@ - (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingFor
[self postNotificationForEventDispatcherObserversWithEvent:event];
}

- (void)willMoveToWindow:(UIWindow *)newWindow
{
if (@available(iOS 26, *)) {
// In iOS 26, as soon as another screen appears in transition, it is interactable
// To avoid glitches resulting from clicking buttons mid transition, we temporarily disable all interactions
// Disabling interactions for parent navigation controller won't be enough in case of nested stack
// Furthermore, a stack put inside a modal will exist in an entirely different hierarchy

// Use RNSViewInteractionManager util to find a suitable subtree to disable interations on,
// starting from reactSuperview
if (![self isPresentedAsNativeModal]) {
[RNSScreenView.viewInteractionManagerInstance disableInteractionsForSubtreeWith:self.reactSuperview];
}
}
}

- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
if (_preventNativeDismiss) {
Expand All @@ -674,11 +647,6 @@ - (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presenta

- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController
{
if (@available(iOS 26, *)) {
// Reenable interactions
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
}

// NOTE(kkafar): We should consider depracating the use of gesture cancel here & align
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "depracating" should be "deprecating".

Suggested change
// NOTE(kkafar): We should consider depracating the use of gesture cancel here & align
// NOTE(kkafar): We should consider deprecating the use of gesture cancel here & align

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to PR

// with usePreventRemove API of react-navigation v7.
[self notifyGestureCancel];
Expand All @@ -689,12 +657,6 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)pr

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if (@available(iOS 26, *)) {
// Reenable interactions
// Dismissed screen doesn't hold a reference to window, but presentingViewController.view does
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
}

[_controller notifyPresentedControllerDismissed];

if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
Expand Down Expand Up @@ -1298,8 +1260,6 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::

[self setSynchronousShadowStateUpdatesEnabled:newScreenProps.synchronousShadowStateUpdatesEnabled];

[RNSScreenView.viewInteractionManagerInstance setDisabled:newScreenProps.ios26AllowInteractionsDuringTransition];

#if !TARGET_OS_TV
if (newScreenProps.statusBarHidden != oldScreenProps.statusBarHidden) {
[self setStatusBarHidden:newScreenProps.statusBarHidden];
Expand Down Expand Up @@ -1541,10 +1501,6 @@ - (void)viewWillDisappear:(BOOL)animated

- (void)viewDidAppear:(BOOL)animated
{
if (@available(iOS 26, *)) {
// Reenable interactions, see willMoveToWindow
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
}
[super viewDidAppear:animated];
if (!_isSwiping || _shouldNotify) {
// we are going forward or dismissing without swipe
Expand Down Expand Up @@ -1581,11 +1537,6 @@ - (void)viewDidDisappear:(BOOL)animated

_isSwiping = NO;
_shouldNotify = YES;

if (@available(iOS 26, *)) {
// Reenable interactions, see willMoveToWindow
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
}
}

- (void)viewDidLayoutSubviews
Expand Down
3 changes: 0 additions & 3 deletions ios/RNSScreenStack.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) BOOL customAnimation;
@property (nonatomic) BOOL disableSwipeBack;

@property (nonatomic, readwrite) BOOL iosPreventReattachmentOfDismissedScreens;
@property (nonatomic, readwrite) BOOL iosPreventReattachmentOfDismissedModals;

@end

#pragma mark-- Integration
Expand Down
50 changes: 2 additions & 48 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
#import "RNSScreenWindowTraits.h"
#import "RNSScrollViewFinder.h"
#import "RNSTabsScreenViewController.h"
#import "RNSViewInteractionAware.h"
#import "UIScrollView+RNScreens.h"
#import "UIView+RNSUtility.h"
#import "integrations/RNSDismissibleModalProtocol.h"
Expand All @@ -32,7 +31,6 @@ @interface RNSScreenStackView () <
UIAdaptivePresentationControllerDelegate,
UIGestureRecognizerDelegate,
UIViewControllerTransitioningDelegate,
RNSViewInteractionAware,
RCTMountingTransactionObserving>

@property (nonatomic) NSMutableArray<UIViewController *> *presentedModals;
Expand Down Expand Up @@ -178,7 +176,6 @@ @implementation RNSScreenStackView {
RNSPercentDrivenInteractiveTransition *_interactionController;
__weak RNSScreenStackManager *_manager;
BOOL _updateScheduled;
UIPanGestureRecognizer *_sinkEventsPanGestureRecognizer;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand Down Expand Up @@ -208,10 +205,7 @@ - (void)initCommonProps
_presentedModals = [NSMutableArray new];
_controller = [RNSNavigationController new];
_controller.delegate = self;
_sinkEventsPanGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
_nativeContainerBackgroundColor = nil;
_iosPreventReattachmentOfDismissedScreens = YES;
_iosPreventReattachmentOfDismissedModals = YES;
#if !TARGET_OS_TV && !TARGET_OS_VISION
[self setupGestureHandlers];
#endif
Expand Down Expand Up @@ -288,12 +282,6 @@ - (void)didMoveToWindow
[super didMoveToWindow];
// for handling nested stacks
[self maybeAddToParentAndUpdateContainer];
if (self.window == nil) {
// When hot reload happens that would remove the whole stack, disabling the interaction on a screen out transition
// will not be matched with enabling the interactions on another screen's in transition. We need to make sure
// that the subtree is interactive again
[RNSScreenView.viewInteractionManagerInstance enableInteractionsForLastSubtree];
}
}

- (void)maybeAddToParentAndUpdateContainer
Expand Down Expand Up @@ -710,8 +698,7 @@ - (void)updateContainer
/// callback fires correctly on the JS side. This breaks the general assumption that a screen
/// removed from the hierarchy will never be reattached.
/// See: https://github.com/software-mansion/react-native-screens/issues/3885
if (_iosPreventReattachmentOfDismissedScreens && screen.controller.isRemovedFromParent &&
!screen.preventNativeDismiss) {
if (screen.controller.isRemovedFromParent && !screen.preventNativeDismiss) {
continue;
}
[pushControllers addObject:screen.controller];
Expand All @@ -724,7 +711,7 @@ - (void)updateContainer
/// Since view recycling is disabled, once we detect that a modal has been removed from the view
/// hierarchy, it won't be reused. This allows us to safely filter out dismissed modal from modals coming
/// from JS state via `controllers`.
if (_iosPreventReattachmentOfDismissedModals && screen.controller.isRemovedFromParent) {
if (screen.controller.isRemovedFromParent) {
continue;
}
[modalControllers addObject:screen.controller];
Expand Down Expand Up @@ -795,20 +782,6 @@ - (void)cancelTouchesInParent
[[self rnscreens_findTouchHandlerInAncestorChain] rnscreens_cancelTouches];
}

- (void)rnscreens_disableInteractions
{
// When transitioning between screens, disable interactions on stack subview which wraps the screens
// and sink all gesture events. This should work for nested stacks and stack inside tabs, inside stack.
self.subviews[0].userInteractionEnabled = NO;
[self addGestureRecognizer:_sinkEventsPanGestureRecognizer];
}

- (void)rnscreens_enableInteractions
{
self.subviews[0].userInteractionEnabled = YES;
[self removeGestureRecognizer:_sinkEventsPanGestureRecognizer];
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (_disableSwipeBack) {
Expand Down Expand Up @@ -1191,15 +1164,6 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if (otherGestureRecognizer == _sinkEventsPanGestureRecognizer) {
// When transition happens between two stack screens, a special "sink" recognizer is added, and then removed.
// It captures all gestures for the time of transition and does nothing, so that in nested stack scenario,
// the outer most stack does not recognize swipe gestures, otherwise it would dismiss the whole nested stack.
// For the recognizer to work as described, it should have precedence over all other recognizers.
// see also: rnscreens_enableInteractions, rnscreens_disableInteractions
return YES;
}

if (@available(iOS 26, *)) {
if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer &&
[self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) {
Expand Down Expand Up @@ -1309,16 +1273,6 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
const auto &oldScreenProps = *std::static_pointer_cast<const react::RNSScreenStackProps>(_props);
const auto &newScreenProps = *std::static_pointer_cast<const react::RNSScreenStackProps>(props);

if (newScreenProps.iosPreventReattachmentOfDismissedScreens !=
oldScreenProps.iosPreventReattachmentOfDismissedScreens) {
[self setIosPreventReattachmentOfDismissedScreens:newScreenProps.iosPreventReattachmentOfDismissedScreens];
}

if (newScreenProps.iosPreventReattachmentOfDismissedModals !=
oldScreenProps.iosPreventReattachmentOfDismissedModals) {
[self setIosPreventReattachmentOfDismissedModals:newScreenProps.iosPreventReattachmentOfDismissedModals];
}

if (newScreenProps.nativeContainerBackgroundColor != oldScreenProps.nativeContainerBackgroundColor) {
_nativeContainerBackgroundColor = RCTUIColorFromSharedColor(newScreenProps.nativeContainerBackgroundColor);
_controller.view.backgroundColor = _nativeContainerBackgroundColor;
Expand Down
9 changes: 0 additions & 9 deletions ios/helpers/RNSViewInteractionAware.h

This file was deleted.

25 changes: 0 additions & 25 deletions ios/helpers/RNSViewInteractionManager.h

This file was deleted.

64 changes: 0 additions & 64 deletions ios/helpers/RNSViewInteractionManager.mm

This file was deleted.

Loading
Loading