Skip to content

Commit 652dbcf

Browse files
alduzykkafar
authored andcommitted
fix(iOS): header subviews layout on tab change (#2385)
## Description This PR intents to fix header subviews incorrect layout when changing tabs. The previous solution did layout the subviews correctly in the test cases, but triggered an undesirable `layoutIfNeeded` when going back from tab to tab. In such case the navigation layout happened without updating subview's layout metrics. Moving the logic to subview resolves the issue as the re-layout is now triggered only when subview's layout metrics are updated. Related fixes from the past: #2316, #2248 ## Changes - combined `Test2231.tsx` with `Test432.tsx` to create comprehensive test case - moved re-layout logic to subview ## Screenshots / GIFs ### Before ![Screenshot 2024-10-04 at 10 10 15](https://github.com/user-attachments/assets/1a8a747e-fe1d-4b03-ba63-1891732d7d77) ### After ![Screenshot 2024-10-04 at 10 09 37](https://github.com/user-attachments/assets/68b3554f-d67d-47f4-946d-ace60e1bbf83) ## Test code and steps to reproduce - Use `Test432.tsx` repro ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes (cherry picked from commit 91d89c4)
1 parent d83d4b7 commit 652dbcf

File tree

5 files changed

+115
-106
lines changed

5 files changed

+115
-106
lines changed

apps/src/tests/Test2231.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.

apps/src/tests/Test432.tsx

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,36 @@
1-
import { Pressable, View, Button, Text } from 'react-native';
2-
3-
import { NavigationContainer, useNavigation } from '@react-navigation/native';
1+
import { View, Button, Text } from 'react-native';
2+
import { NavigationContainer } from '@react-navigation/native';
43
import {
54
NativeStackScreenProps,
65
createNativeStackNavigator,
76
} from '@react-navigation/native-stack';
8-
import React, { useCallback } from 'react';
7+
import React, { useEffect, useLayoutEffect, useState } from 'react';
8+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
9+
import { Square } from '../shared';
910

10-
type RootStackParamList = {
11+
type StackParamList = {
1112
Home: undefined;
13+
Details: undefined;
1214
Settings: undefined;
15+
Info: undefined;
1316
};
14-
type RootStackScreenProps<T extends keyof RootStackParamList> =
15-
NativeStackScreenProps<RootStackParamList, T>;
1617

17-
const HomeScreen = ({ navigation }: RootStackScreenProps<'Home'>) => {
18-
const [x, setX] = React.useState(false);
19-
React.useEffect(() => {
20-
navigation.setOptions({
21-
headerBackVisible: !x,
22-
headerRight: x
23-
? () => (
24-
<View style={{ backgroundColor: 'green', width: 20, height: 20 }} />
25-
)
26-
: () => (
27-
<View style={{ backgroundColor: 'green', width: 10, height: 10 }} />
28-
),
29-
});
30-
}, [navigation, x]);
18+
type StackScreenProps<T extends keyof StackParamList> = NativeStackScreenProps<
19+
StackParamList,
20+
T
21+
>;
3122

23+
const HomeScreen = ({ navigation }: StackScreenProps<'Home'>) => {
3224
return (
3325
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
34-
<Button title="Tap me for header update" onPress={() => setX(!x)} />
26+
<Button
27+
title={'Go to details'}
28+
onPress={() => navigation.navigate('Details')}
29+
/>
30+
<Button
31+
title={'Go to info'}
32+
onPress={() => navigation.navigate('Info')}
33+
/>
3534
<Button
3635
title={'Show settings'}
3736
onPress={() => navigation.navigate('Settings')}
@@ -40,49 +39,95 @@ const HomeScreen = ({ navigation }: RootStackScreenProps<'Home'>) => {
4039
);
4140
};
4241

42+
const DetailsScreen = ({ navigation }: StackScreenProps<'Details'>) => {
43+
const [x, setX] = useState(false);
44+
useEffect(() => {
45+
navigation.setOptions({
46+
headerBackVisible: !x,
47+
headerRight: () =>
48+
x ? <Square size={20} color="green" /> : <Square size={10} />,
49+
});
50+
}, [navigation, x]);
51+
52+
return <Button title="Toggle subviews" onPress={() => setX(prev => !prev)} />;
53+
};
54+
4355
const SettingsScreen = () => {
44-
return (
45-
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
46-
<Text>Settings</Text>
56+
return <Text>Settings</Text>;
57+
};
58+
59+
const InfoScreen = ({ navigation }: StackScreenProps<'Info'>) => {
60+
const [hasLeftItem, setHasLeftItem] = useState(false);
61+
62+
const square1 = (props: { tintColor?: string }) => (
63+
<View style={{ gap: 8, flexDirection: 'row' }}>
64+
{hasLeftItem && <Square {...props} color="green" size={20} />}
65+
<Square {...props} color="green" size={20} />
4766
</View>
4867
);
49-
};
5068

51-
const RootStack = createNativeStackNavigator<RootStackParamList>();
52-
const RootNavigator = () => {
53-
const navigation = useNavigation();
54-
const headerRight = useCallback(
55-
() => (
56-
<Pressable
57-
onPress={() => {
58-
navigation.goBack();
59-
}}>
60-
<Text>Close</Text>
61-
</Pressable>
62-
),
63-
[navigation],
69+
const square2 = (props: { tintColor?: string }) => (
70+
<Square {...props} color="red" size={20} />
6471
);
72+
73+
useLayoutEffect(() => {
74+
navigation.setOptions({
75+
headerRight: square1,
76+
headerTitle: undefined,
77+
headerLeft: hasLeftItem ? square2 : undefined,
78+
});
79+
}, [navigation, hasLeftItem]);
80+
6581
return (
66-
<RootStack.Navigator>
67-
<RootStack.Screen name="Home" component={HomeScreen} />
68-
<RootStack.Screen
82+
<Button
83+
title="Toggle subviews"
84+
onPress={() => setHasLeftItem(prev => !prev)}
85+
/>
86+
);
87+
};
88+
89+
const Stack = createNativeStackNavigator<StackParamList>();
90+
91+
const StackNavigator = () => {
92+
return (
93+
<Stack.Navigator>
94+
<Stack.Screen
95+
name="Home"
96+
component={HomeScreen}
97+
options={{
98+
headerRight: () => <Square size={20} color="black" />,
99+
}}
100+
/>
101+
<Stack.Screen name="Details" component={DetailsScreen} />
102+
<Stack.Screen
103+
name="Info"
104+
component={InfoScreen}
105+
options={{
106+
headerTintColor: 'hotpink',
107+
}}
108+
/>
109+
<Stack.Screen
69110
name="Settings"
70111
component={SettingsScreen}
71112
options={{
72113
presentation: 'modal',
73114
animation: 'slide_from_bottom',
74-
headerShown: true,
75-
headerRight: headerRight,
115+
headerRight: () => <Square size={30} />,
76116
}}
77117
/>
78-
</RootStack.Navigator>
118+
</Stack.Navigator>
79119
);
80120
};
81121

122+
const Tabs = createBottomTabNavigator();
123+
82124
export default function App() {
83125
return (
84126
<NavigationContainer>
85-
<RootNavigator />
127+
<Tabs.Navigator screenOptions={{ headerShown: false }}>
128+
<Tabs.Screen name="First" component={StackNavigator} />
129+
<Tabs.Screen name="Second" component={StackNavigator} />
130+
</Tabs.Navigator>
86131
</NavigationContainer>
87132
);
88133
}

apps/src/tests/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ export { default as Test2184 } from './Test2184';
105105
export { default as Test2223 } from './Test2223';
106106
export { default as Test2227 } from './Test2227';
107107
export { default as Test2229 } from './Test2229';
108-
export { default as Test2231 } from './Test2231';
109108
export { default as Test2232 } from './Test2232';
110109
export { default as Test2235 } from './Test2235';
111110
export { default as Test2252 } from './Test2252';

ios/RNSScreenStackHeaderConfig.mm

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -689,9 +689,6 @@ + (void)updateViewController:(UIViewController *)vc
689689
break;
690690
}
691691
}
692-
// We're forcing a re-layout when the subviews change,
693-
// see: https://github.com/software-mansion/react-native-screens/pull/2316
694-
[navctr.view layoutIfNeeded];
695692
}
696693

697694
// This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug).

ios/RNSScreenStackHeaderSubview.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import "RNSScreenStackHeaderSubview.h"
22
#import "RNSConvert.h"
3+
#import "RNSScreenStackHeaderConfig.h"
34

45
#ifdef RCT_NEW_ARCH_ENABLED
56
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
@@ -22,6 +23,27 @@ @implementation RNSScreenStackHeaderSubview
2223

2324
#pragma mark - Common
2425

26+
- (nullable RNSScreenStackHeaderConfig *)getHeaderConfig
27+
{
28+
RNSScreenStackHeaderConfig *headerConfig = (RNSScreenStackHeaderConfig *_Nullable)self.reactSuperview;
29+
#ifndef NDEBUG
30+
if (headerConfig != nil && ![headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
31+
RCTLogError(@"[RNScreens] Invalid view type, expecting RNSScreenStackHeaderConfig, got: %@", headerConfig);
32+
return nil;
33+
}
34+
#endif
35+
return headerConfig;
36+
}
37+
38+
// We're forcing the navigation controller's view to re-layout
39+
// see: https://github.com/software-mansion/react-native-screens/pull/2385
40+
- (void)layoutNavigationBarIfNeeded
41+
{
42+
RNSScreenStackHeaderConfig *headerConfig = [self getHeaderConfig];
43+
UINavigationController *navctr = headerConfig.screenView.reactViewController.navigationController;
44+
[navctr.navigationBar layoutIfNeeded];
45+
}
46+
2547
#ifdef RCT_NEW_ARCH_ENABLED
2648

2749
#pragma mark - Fabric specific
@@ -78,6 +100,7 @@ - (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
78100
self);
79101
} else {
80102
self.bounds = CGRect{CGPointZero, frame.size};
103+
[self layoutNavigationBarIfNeeded];
81104
}
82105
}
83106

@@ -102,6 +125,7 @@ - (void)reactSetFrame:(CGRect)frame
102125
// Block any attempt to set coordinates on RNSScreenStackHeaderSubview. This
103126
// makes UINavigationBar the only one to control the position of header content.
104127
[super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
128+
[self layoutNavigationBarIfNeeded];
105129
}
106130

107131
#endif // RCT_NEW_ARCH_ENABLED

0 commit comments

Comments
 (0)