Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(iOS): white flash on tab change when using native stack #2188

Merged
merged 10 commits into from
Jul 9, 2024
Merged
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
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;
alduzy marked this conversation as resolved.
Show resolved Hide resolved
#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
Loading