Skip to content

Commit f05918b

Browse files
authored
feat(iOS 26): Add opt-out flag for interactions during transition for iOS 26 (#3631)
## Description Closes software-mansion/react-native-screens-labs#937 It turns out that the fix in #3584 enables us to retire the logic introduced in #3093 and #3173. We no longer need to block interactions on screen transitions on iOS 26, so apps should feel more responsive. For now, we introduce a opt-out feature flag to go back to the previous behavior. > [!important] > > This flag should really be enabled if `iosPreventReattachmentOfDismissedScreens` is also enabled (which is the default), otherwise the navigation state will not synchronize correctly and there will be visual bugs when navigating between screens. ## Changes Added a `disabled` state for `RNSViewInteractionManager` which is controlled by the `ios26AllowInteractionsDuringTransition` feature flag. ## Before & after - visual documentation | Before | After | | --- | --- | | <video src="https://github.com/user-attachments/assets/25d3070f-687d-4466-8374-c7d5983019f6" /> | <video src="https://github.com/user-attachments/assets/84b1f0d0-53b1-4f1f-b328-9b2077a22dac" /> | ## Test plan Use Test3093. Verify that pushing/dismissing screens repeatedly works correctly & doesn't cause JS/native synchronization issues. ## Checklist - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] Ensured that CI passes
1 parent 35e9851 commit f05918b

File tree

11 files changed

+78
-0
lines changed

11 files changed

+78
-0
lines changed

Example/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
import App from '../apps';
22

3+
import { featureFlags } from 'react-native-screens';
4+
5+
featureFlags.experiment.iosPreventReattachmentOfDismissedScreens = true;
6+
featureFlags.experiment.ios26AllowInteractionsDuringTransition = true;
7+
38
export default App;

FabricExample/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ featureFlags.experiment.synchronousHeaderConfigUpdatesEnabled = false;
66
featureFlags.experiment.synchronousHeaderSubviewUpdatesEnabled = false;
77
featureFlags.experiment.androidResetScreenShadowStateOnOrientationChangeEnabled =
88
true;
9+
featureFlags.experiment.iosPreventReattachmentOfDismissedScreens = true;
10+
featureFlags.experiment.ios26AllowInteractionsDuringTransition = true;
911

1012
export default App;

android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@ open class ScreenViewManager :
357357
value: Boolean,
358358
) = Unit
359359

360+
override fun setIos26AllowInteractionsDuringTransition(
361+
view: Screen?,
362+
value: Boolean,
363+
) = Unit
364+
360365
// END mark: iOS-only
361366

362367
override fun setAndroidResetScreenShadowStateOnOrientationChangeEnabled(

android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
146146
case "androidResetScreenShadowStateOnOrientationChangeEnabled":
147147
mViewManager.setAndroidResetScreenShadowStateOnOrientationChangeEnabled(view, value == null ? true : (boolean) value);
148148
break;
149+
case "ios26AllowInteractionsDuringTransition":
150+
mViewManager.setIos26AllowInteractionsDuringTransition(view, value == null ? true : (boolean) value);
151+
break;
149152
default:
150153
super.setProperty(view, propName, value);
151154
}

android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ public interface RNSScreenManagerInterface<T extends View> extends ViewManagerWi
5656
void setTopScrollEdgeEffect(T view, @Nullable String value);
5757
void setSynchronousShadowStateUpdatesEnabled(T view, boolean value);
5858
void setAndroidResetScreenShadowStateOnOrientationChangeEnabled(T view, boolean value);
59+
void setIos26AllowInteractionsDuringTransition(T view, boolean value);
5960
}

ios/RNSScreen.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::
14161416

14171417
[self setSynchronousShadowStateUpdatesEnabled:newScreenProps.synchronousShadowStateUpdatesEnabled];
14181418

1419+
[RNSScreenView.viewInteractionManagerInstance setDisabled:newScreenProps.ios26AllowInteractionsDuringTransition];
1420+
14191421
#if !TARGET_OS_TV
14201422
if (newScreenProps.statusBarHidden != oldScreenProps.statusBarHidden) {
14211423
[self setStatusBarHidden:newScreenProps.statusBarHidden];
@@ -1595,6 +1597,13 @@ - (void)reactSetFrame:(CGRect)frame
15951597
// subviews
15961598
}
15971599

1600+
// Opt-in/-out of interaction disabling on transition
1601+
// See: https://github.com/software-mansion/react-native-screens/pull/3631
1602+
- (void)setIos26AllowInteractionsDuringTransition:(BOOL)value
1603+
{
1604+
[RNSScreenView.viewInteractionManagerInstance setDisabled:value];
1605+
}
1606+
15981607
#endif
15991608

16001609
@end
@@ -2269,6 +2278,7 @@ @implementation RNSScreenManager
22692278
RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock);
22702279
RCT_EXPORT_VIEW_PROPERTY(onGestureCancel, RCTDirectEventBlock);
22712280
RCT_EXPORT_VIEW_PROPERTY(onSheetDetentChanged, RCTDirectEventBlock);
2281+
RCT_EXPORT_VIEW_PROPERTY(ios26AllowInteractionsDuringTransition, BOOL);
22722282

22732283
#if !TARGET_OS_TV
22742284
RCT_EXPORT_VIEW_PROPERTY(screenOrientation, UIInterfaceOrientationMask)

ios/helpers/RNSViewInteractionManager.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
@interface RNSViewInteractionManager : NSObject
44

5+
/**
6+
* The behavior is controlled by `ios26AllowInteractionsDuringTransition` feature flag.
7+
* The default is `true`, which means that `disableInteractions`/`enableInteractions` do nothing.
8+
*/
9+
@property (nonatomic) BOOL disabled;
10+
511
- (instancetype)init;
612

713
/**

ios/helpers/RNSViewInteractionManager.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ - (instancetype)init
1010
{
1111
if (self = [super init]) {
1212
lastRootWithInteractionsDisabled = nil;
13+
_disabled = YES;
1314
}
1415
return self;
1516
}
1617

1718
- (void)disableInteractionsForSubtreeWith:(UIView *)view
1819
{
20+
if (_disabled) {
21+
return;
22+
}
23+
1924
UIView *current = view;
2025
while (current && ![current isKindOfClass:UIWindow.class] &&
2126
![current respondsToSelector:@selector(rnscreens_disableInteractions)]) {
@@ -41,6 +46,10 @@ - (void)disableInteractionsForSubtreeWith:(UIView *)view
4146

4247
- (void)enableInteractionsForLastSubtree
4348
{
49+
if (_disabled) {
50+
return;
51+
}
52+
4453
if (lastRootWithInteractionsDisabled) {
4554
if ([lastRootWithInteractionsDisabled respondsToSelector:@selector(rnscreens_enableInteractions)]) {
4655
[static_cast<id<RNSViewInteractionAware>>(lastRootWithInteractionsDisabled) rnscreens_enableInteractions];

src/components/Screen.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from './helpers/sheet';
3131
import { parseBooleanToOptionalBooleanNativeProp } from '../utils';
3232
import featureFlags from '../flags';
33+
import warnOnce from 'warn-once';
3334

3435
type NativeProps = ScreenNativeComponentProps | ModalScreenNativeComponentProps;
3536
const AnimatedNativeScreen = Animated.createAnimatedComponent(
@@ -163,6 +164,13 @@ export const InnerScreen = React.forwardRef<View, ScreenProps>(
163164
activityState = active !== 0 ? 2 : 0; // in the new version, we need one of the screens to have value of 2 after the transition
164165
}
165166

167+
warnOnce(
168+
Platform.OS === 'ios' &&
169+
featureFlags.experiment.ios26AllowInteractionsDuringTransition &&
170+
!featureFlags.experiment.iosPreventReattachmentOfDismissedScreens,
171+
'[RNScreens] Using featureFlags `ios26AllowInteractionsDuringTransition` with `iosPreventReattachmentOfDismissedScreens` disabled is discouraged and will result in visual bugs on screen transitions. See flags description for details.',
172+
);
173+
166174
if (
167175
isNativeStack &&
168176
prevActivityState !== undefined &&
@@ -277,6 +285,9 @@ export const InnerScreen = React.forwardRef<View, ScreenProps>(
277285
androidResetScreenShadowStateOnOrientationChangeEnabled={
278286
featureFlags.experiment
279287
.androidResetScreenShadowStateOnOrientationChangeEnabled
288+
}
289+
ios26AllowInteractionsDuringTransition={
290+
featureFlags.experiment.ios26AllowInteractionsDuringTransition
280291
}>
281292
{!isNativeStack ? ( // see comment of this prop in types.tsx for information why it is needed
282293
children

src/fabric/ScreenNativeComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface NativeProps extends ViewProps {
118118
boolean,
119119
true
120120
>;
121+
ios26AllowInteractionsDuringTransition?: CT.WithDefault<boolean, true>;
121122
}
122123

123124
export default codegenNativeComponent<NativeProps>('RNSScreen', {

0 commit comments

Comments
 (0)