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): header subviews layout on tab change #2385

Merged
merged 7 commits into from
Oct 7, 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
56 changes: 0 additions & 56 deletions apps/src/tests/Test2231.tsx

This file was deleted.

137 changes: 91 additions & 46 deletions apps/src/tests/Test432.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import { Pressable, View, Button, Text } from 'react-native';

import { NavigationContainer, useNavigation } from '@react-navigation/native';
import { View, Button, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import {
NativeStackScreenProps,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
import React, { useCallback } from 'react';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Square } from '../shared';

type RootStackParamList = {
type StackParamList = {
Home: undefined;
Details: undefined;
Settings: undefined;
Info: undefined;
};
type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;

const HomeScreen = ({ navigation }: RootStackScreenProps<'Home'>) => {
const [x, setX] = React.useState(false);
React.useEffect(() => {
navigation.setOptions({
headerBackVisible: !x,
headerRight: x
? () => (
<View style={{ backgroundColor: 'green', width: 20, height: 20 }} />
)
: () => (
<View style={{ backgroundColor: 'green', width: 10, height: 10 }} />
),
});
}, [navigation, x]);
type StackScreenProps<T extends keyof StackParamList> = NativeStackScreenProps<
StackParamList,
T
>;

const HomeScreen = ({ navigation }: StackScreenProps<'Home'>) => {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Tap me for header update" onPress={() => setX(!x)} />
<Button
title={'Go to details'}
onPress={() => navigation.navigate('Details')}
/>
<Button
title={'Go to info'}
onPress={() => navigation.navigate('Info')}
/>
<Button
title={'Show settings'}
onPress={() => navigation.navigate('Settings')}
Expand All @@ -40,49 +39,95 @@ const HomeScreen = ({ navigation }: RootStackScreenProps<'Home'>) => {
);
};

const DetailsScreen = ({ navigation }: StackScreenProps<'Details'>) => {
const [x, setX] = useState(false);
useEffect(() => {
navigation.setOptions({
headerBackVisible: !x,
headerRight: () =>
x ? <Square size={20} color="green" /> : <Square size={10} />,
});
}, [navigation, x]);

return <Button title="Toggle subviews" onPress={() => setX(prev => !prev)} />;
};

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

const InfoScreen = ({ navigation }: StackScreenProps<'Info'>) => {
const [hasLeftItem, setHasLeftItem] = useState(false);

const square1 = (props: { tintColor?: string }) => (
<View style={{ gap: 8, flexDirection: 'row' }}>
{hasLeftItem && <Square {...props} color="green" size={20} />}
<Square {...props} color="green" size={20} />
</View>
);
};

const RootStack = createNativeStackNavigator<RootStackParamList>();
const RootNavigator = () => {
const navigation = useNavigation();
const headerRight = useCallback(
() => (
<Pressable
onPress={() => {
navigation.goBack();
}}>
<Text>Close</Text>
</Pressable>
),
[navigation],
const square2 = (props: { tintColor?: string }) => (
<Square {...props} color="red" size={20} />
);

useLayoutEffect(() => {
navigation.setOptions({
headerRight: square1,
headerTitle: undefined,
headerLeft: hasLeftItem ? square2 : undefined,
});
}, [navigation, hasLeftItem]);

return (
<RootStack.Navigator>
<RootStack.Screen name="Home" component={HomeScreen} />
<RootStack.Screen
<Button
title="Toggle subviews"
onPress={() => setHasLeftItem(prev => !prev)}
/>
);
};

const Stack = createNativeStackNavigator<StackParamList>();

const StackNavigator = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
headerRight: () => <Square size={20} color="black" />,
}}
/>
<Stack.Screen name="Details" component={DetailsScreen} />
<Stack.Screen
name="Info"
component={InfoScreen}
options={{
headerTintColor: 'hotpink',
}}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
headerShown: true,
headerRight: headerRight,
headerRight: () => <Square size={30} />,
}}
/>
</RootStack.Navigator>
</Stack.Navigator>
);
};

const Tabs = createBottomTabNavigator();

export default function App() {
return (
<NavigationContainer>
<RootNavigator />
<Tabs.Navigator screenOptions={{ headerShown: false }}>
<Tabs.Screen name="First" component={StackNavigator} />
<Tabs.Screen name="Second" component={StackNavigator} />
</Tabs.Navigator>
</NavigationContainer>
);
}
1 change: 0 additions & 1 deletion apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ export { default as Test2184 } from './Test2184';
export { default as Test2223 } from './Test2223';
export { default as Test2227 } from './Test2227';
export { default as Test2229 } from './Test2229';
export { default as Test2231 } from './Test2231';
export { default as Test2232 } from './Test2232';
export { default as Test2235 } from './Test2235';
export { default as Test2252 } from './Test2252';
Expand Down
3 changes: 0 additions & 3 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -681,9 +681,6 @@ + (void)updateViewController:(UIViewController *)vc
break;
}
}
// We're forcing a re-layout when the subviews change,
// see: https://github.com/software-mansion/react-native-screens/pull/2316
[navctr.view layoutIfNeeded];
}

// This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug).
Expand Down
24 changes: 24 additions & 0 deletions ios/RNSScreenStackHeaderSubview.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "RNSScreenStackHeaderSubview.h"
#import "RNSConvert.h"
#import "RNSScreenStackHeaderConfig.h"

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

#pragma mark - Common

- (nullable RNSScreenStackHeaderConfig *)getHeaderConfig
{
RNSScreenStackHeaderConfig *headerConfig = (RNSScreenStackHeaderConfig *_Nullable)self.reactSuperview;
#ifndef NDEBUG
if (headerConfig != nil && ![headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
RCTLogError(@"[RNScreens] Invalid view type, expecting RNSScreenStackHeaderConfig, got: %@", headerConfig);
return nil;
}
#endif
return headerConfig;
}

// We're forcing the navigation controller's view to re-layout
// see: https://github.com/software-mansion/react-native-screens/pull/2385
- (void)layoutNavigationBarIfNeeded
{
RNSScreenStackHeaderConfig *headerConfig = [self getHeaderConfig];
UINavigationController *navctr = headerConfig.screenView.reactViewController.navigationController;
[navctr.navigationBar layoutIfNeeded];
}

#ifdef RCT_NEW_ARCH_ENABLED

#pragma mark - Fabric specific
Expand Down Expand Up @@ -78,6 +100,7 @@ - (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
self);
} else {
self.bounds = CGRect{CGPointZero, frame.size};
[self layoutNavigationBarIfNeeded];
}
}

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

#endif // RCT_NEW_ARCH_ENABLED
Expand Down
Loading