Skip to content

Commit

Permalink
fix(iOS): white flash on tab change when using native stack (#2188)
Browse files Browse the repository at this point in the history
## Description

This PR gets rid of undesired white flashes during
`maybeAddToParentAndUpdateContainer`. The white flash was present on
paper architecture when `unmountOnBlur` option was set to true on parent
bottomStackNavigator (see repro).

The affected logic was previously introduced or changed by following
PRs:
- #600
- #613
- #643


The removed `_hasLayout` was initially added by
#600 in
order to resolve an issue:
#432.
However the logic was later changed by
#613 in
order to fix another issue and the added `_hasLayout` may not fix
anything eventually, as stated by [this
comment](#432 (comment)).

Fixes #1645.

## Changes

- removed `_hasLayout` variable
- added repros

## Screenshots / GIFs

### Before


https://github.com/software-mansion/react-native-screens/assets/91994767/226a32d7-728b-48dd-b21a-6a1e4195add2

### After


https://github.com/software-mansion/react-native-screens/assets/91994767/32febcf1-d159-4a9d-ae3a-373042732a6d

## Test code and steps to reproduce

- added `Test1645.js` repro to test examples
- added `Test432.tsx` repro to test examples

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
  • Loading branch information
alduzy and kkafar authored Jul 9, 2024
1 parent 546e72f commit b4aa349
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 15 deletions.
2 changes: 2 additions & 0 deletions apps/test-examples/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Test111 from './src/Test111';
import Test263 from './src/Test263';
import Test349 from './src/Test349';
import Test364 from './src/Test364';
import Test432 from './src/Test432';
import Test528 from './src/Test528';
import Test550 from './src/Test550';
import Test556 from './src/Test556';
Expand Down Expand Up @@ -84,6 +85,7 @@ import Test1473 from './src/Test1473';
import Test1476 from './src/Test1476';
import Test1509 from './src/Test1509';
import Test1539 from './src/Test1539';
import Test1645 from './src/Test1645';
import Test1646 from './src/Test1646';
import Test1649 from './src/Test1649';
import Test1671 from './src/Test1671';
Expand Down
98 changes: 98 additions & 0 deletions apps/test-examples/src/Test1645.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* eslint-disable react-native/no-inline-styles */

import React, {useEffect, useState} from 'react';
import {Text, View} from 'react-native';

import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {createStackNavigator} from '@react-navigation/stack';
import {NavigationContainer} from '@react-navigation/native';

const TestBottomTabBar = createBottomTabNavigator();
const TestNativeStack1 = createNativeStackNavigator();
const TestNativeStack2 = createNativeStackNavigator();

const TestScreen1 = () => {
const [t, setT] = useState(110);

useEffect(() => {
const interval = setInterval(() => {
setT(lastT => lastT + 1);
}, 100);

return () => clearInterval(interval);
}, []);
return (
<View
style={{
flex: 1,
backgroundColor: '#000',
}}>
{Array.from({length: 100}).map((e, idx) => (
<Text style={{color: '#fff'}} key={idx}>
T{idx}: {t}
</Text>
))}
</View>
);
};

const TestScreen2 = () => {
const [t, setT] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setT(lastT => lastT + 1);
}, 100);

return () => clearInterval(interval);
}, []);

return (
<View
style={{
flex: 1,
backgroundColor: '#000',
}}>
{Array.from({length: 100}).map((e, idx) => (
<Text style={{color: 'red'}} key={idx}>
T{idx}: {t}
</Text>
))}
</View>
);
};
const TestScreenTab1 = () => {
return (
<TestNativeStack1.Navigator initialRouteName="screen1a">
<TestNativeStack1.Screen name="screen1a" component={TestScreen1} />
<TestNativeStack1.Screen name="screen1b" component={TestScreen2} />
</TestNativeStack1.Navigator>
);
};

const TestScreenTab2 = () => {
return (
<TestNativeStack2.Navigator initialRouteName="screen2a">
<TestNativeStack2.Screen name="screen2a" component={TestScreen2} />
<TestNativeStack2.Screen name="screen2b" component={TestScreen1} />
</TestNativeStack2.Navigator>
);
};

const App = () => {
return (
<NavigationContainer>
<TestBottomTabBar.Navigator
initialRouteName="tab1"
screenOptions={{
unmountOnBlur: true,
}}>
<TestBottomTabBar.Screen name="tab1" component={TestScreenTab1} />
<TestBottomTabBar.Screen name="tab2" component={TestScreenTab2} />
</TestBottomTabBar.Navigator>
</NavigationContainer>
);
};

export default App;
72 changes: 72 additions & 0 deletions apps/test-examples/src/Test432.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Pressable, View, Button, Text } from 'react-native';

import { NavigationContainer, useNavigation } from '@react-navigation/native';
import {
NativeStackScreenProps,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
import React, { useCallback } from 'react';

type RootStackParamList = {
Home: undefined;
Settings: undefined;
};
type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
const HomeScreen = ({ navigation }: RootStackScreenProps<'Home'>) => {
const showSettings = useCallback(() => {
navigation.navigate('Settings');
}, [navigation]);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button onPress={showSettings} title={'Show settings'} />
</View>
);
};

const SettingsScreen = ({ navigation }: RootStackScreenProps<'Settings'>) => {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Settings</Text>
</View>
);
};

const RootStack = createNativeStackNavigator<RootStackParamList>();
const RootNavigator = () => {
const navigation = useNavigation();
const headerRight = useCallback(
() => (
<Pressable
onPress={() => {
navigation.goBack();
}}>
<Text>Close</Text>
</Pressable>
),
[navigation],
);
return (
<RootStack.Navigator screenOptions={{ headerShown: false }}>
<RootStack.Screen name="Home" component={HomeScreen} />
<RootStack.Screen
name="Settings"
component={SettingsScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
headerShown: true,
headerRight: headerRight,
}}
/>
</RootStack.Navigator>
);
};

export default function App() {
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
}
18 changes: 3 additions & 15 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ @implementation RNSScreenStackView {
BOOL _invalidated;
BOOL _isFullWidthSwiping;
UIPercentDrivenInteractiveTransition *_interactionController;
BOOL _hasLayout;
__weak RNSScreenStackManager *_manager;
BOOL _updateScheduled;
#ifdef RCT_NEW_ARCH_ENABLED
Expand All @@ -142,7 +141,6 @@ - (instancetype)initWithFrame:(CGRect)frame
- (instancetype)initWithManager:(RNSScreenStackManager *)manager
{
if (self = [super init]) {
_hasLayout = NO;
_invalidated = NO;
_manager = manager;
[self initCommonProps];
Expand Down Expand Up @@ -272,18 +270,9 @@ - (void)didMoveToWindow
- (void)maybeAddToParentAndUpdateContainer
{
BOOL wasScreenMounted = _controller.parentViewController != nil;
#ifdef RCT_NEW_ARCH_ENABLED
BOOL isScreenReadyForShowing = self.window;
#else
BOOL isScreenReadyForShowing = self.window && _hasLayout;
#endif
if (!isScreenReadyForShowing && !wasScreenMounted) {
// We wait with adding to parent controller until the stack is mounted and has its initial
// layout done.
// If we add it before layout, some of the items (specifically items from the navigation bar),
// won't be able to position properly. Also the position and size of such items, even if it
// happens to change, won't be properly updated (this is perhaps some internal issue of UIKit).
// If we add it when window is not attached, some of the view transitions will be bloced (i.e.
if (!self.window && !wasScreenMounted) {
// We wait with adding to parent controller until the stack is mounted.
// If we add it when window is not attached, some of the view transitions will be blocked (i.e.
// modal transitions) and the internal view controler's state will get out of sync with what's
// on screen without us knowing.
return;
Expand Down Expand Up @@ -1076,7 +1065,6 @@ - (void)didUpdateReactSubviews
// set yet, however the layout call is already enqueued on ui thread. Enqueuing update call on the
// ui queue will guarantee that the update will run after layout.
dispatch_async(dispatch_get_main_queue(), ^{
self->_hasLayout = YES;
[self maybeAddToParentAndUpdateContainer];
});
}
Expand Down

0 comments on commit b4aa349

Please sign in to comment.