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 snapshots not working #2393

Merged
merged 13 commits into from
Oct 15, 2024
105 changes: 75 additions & 30 deletions apps/src/tests/Test556.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import * as React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack';
import {
NativeStackNavigationProp,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
import { Square } from '../shared';

const Stack = createNativeStackNavigator();

type ScreenBaseProps = {
navigation: NativeStackNavigationProp<ParamListBase>;
}
};

export default function App() {
return (
Expand All @@ -16,30 +20,48 @@ export default function App() {
screenOptions={{
animation: 'fade',
}}>
<Stack.Screen name="First" component={First} options={{
headerTitle: () => (
<View style={[styles.container, { backgroundColor: 'goldenrod' }]}>
<Text>Hello there!</Text>
</View>
),
headerRight: () => (
<View style={[styles.container, { backgroundColor: 'lightblue' }]}>
<Text>Right-1</Text>
</View>
),
}} />
<Stack.Screen name="Second" component={Second} options={{
headerTitle: () => (
<View style={[styles.container, { backgroundColor: 'mediumseagreen' }]}>
<Text>General Kenobi</Text>
</View>
),
headerRight: () => (
<View style={[styles.container, { backgroundColor: 'mediumvioletred' }]}>
<Text>Right-2</Text>
</View>
),
}} />
<Stack.Screen
name="First"
component={First}
options={{
headerTitle: () => (
<View
style={[styles.container, { backgroundColor: 'goldenrod' }]}>
<Text>Hello there!</Text>
</View>
),
headerRight: () => (
<View
style={[styles.container, { backgroundColor: 'lightblue' }]}>
<Text>Right-1</Text>
</View>
),
}}
/>
<Stack.Screen
name="Second"
component={Second}
options={{
headerTitle: () => (
<View
style={[
styles.container,
{ backgroundColor: 'mediumseagreen' },
]}>
<Text>General Kenobi</Text>
</View>
),
headerRight: () => (
<View
style={[
styles.container,
{ backgroundColor: 'mediumvioletred' },
]}>
<Text>Right-2</Text>
</View>
),
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
Expand All @@ -55,11 +77,34 @@ function First({ navigation }: ScreenBaseProps) {
}

function Second({ navigation }: ScreenBaseProps) {
const [backButtonVisible, setBackButtonVisible] = React.useState(true);

return (
<Button
title="Tap me for first screen"
onPress={() => navigation.popTo('First')}
/>
<>
<Button
title="Toggle left subview"
onPress={() => {
setBackButtonVisible(prev => !prev);
navigation.setOptions({
headerLeft: backButtonVisible
? () => (
<View
style={[
styles.container,
{ backgroundColor: 'mediumblue' },
]}>
<Text>Left</Text>
</View>
)
: undefined,
});
}}
/>
<Button
title="Tap me for first screen"
onPress={() => navigation.popTo('First')}
/>
</>
);
}

Expand Down
6 changes: 6 additions & 0 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ namespace react = facebook::react;
@property (nonatomic) react::LayoutMetrics newLayoutMetrics;
@property (weak, nonatomic) RNSScreenStackHeaderConfig *config;
@property (nonatomic, readonly) BOOL hasHeaderConfig;
@property (nonatomic, readonly, getter=isMarkedForUnmountInCurrentTransaction)
BOOL markedForUnmountInCurrentTransaction;
#else
@property (nonatomic, copy) RCTDirectEventBlock onAppear;
@property (nonatomic, copy) RCTDirectEventBlock onDisappear;
Expand All @@ -124,6 +126,10 @@ namespace react = facebook::react;
- (void)updateBounds;
- (void)notifyDismissedWithCount:(int)dismissCount;
- (instancetype)initWithFrame:(CGRect)frame;
/// Tell `Screen` that it will be unmounted in next transaction.
/// The component needs this so that we can later decide whether to
/// replace it with snapshot or not.
- (void)willBeUnmountedInUpcomingTransaction;
#else
- (instancetype)initWithBridge:(RCTBridge *)bridge;
#endif // RCT_NEW_ARCH_ENABLED
Expand Down
8 changes: 8 additions & 0 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ - (void)initCommonProps
#endif // !TARGET_OS_TV
_sheetsScrollView = nil;
_didSetSheetAllowedDetentsOnController = NO;
#ifdef RCT_NEW_ARCH_ENABLED
_markedForUnmountInCurrentTransaction = NO;
#endif // RCT_NEW_ARCH_ENABLED
}

- (UIViewController *)reactViewController
Expand Down Expand Up @@ -1082,6 +1085,11 @@ - (BOOL)hasHeaderConfig
return _config != nil;
}

- (void)willBeUnmountedInUpcomingTransaction
{
_markedForUnmountInCurrentTransaction = YES;
}

+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenComponentDescriptor>();
Expand Down
1 change: 1 addition & 0 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,7 @@ - (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction
if (mutation.type == react::ShadowViewMutation::Delete) {
RNSScreenView *_Nullable toBeRemovedChild = [self childScreenForTag:mutation.oldChildShadowView.tag];
if (toBeRemovedChild != nil) {
[toBeRemovedChild willBeUnmountedInUpcomingTransaction];
_toBeDeletedScreens.push_back(toBeRemovedChild);
}
}
Expand Down
13 changes: 9 additions & 4 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -823,12 +823,17 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// For explanation of why we can make a snapshot here despite the fact that our children are already
// unmounted see https://github.com/software-mansion/react-native-screens/pull/2261
[self replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubview *)childComponentView];
BOOL isGoingToBeRemoved = _screenView.isMarkedForUnmountInCurrentTransaction;
if (isGoingToBeRemoved) {
// For explanation of why we can make a snapshot here despite the fact that our children are already
// unmounted see https://github.com/software-mansion/react-native-screens/pull/2261
[self replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubview *)childComponentView];
}
[_reactSubviews removeObject:(RNSScreenStackHeaderSubview *)childComponentView];
[childComponentView removeFromSuperview];
[self updateViewControllerIfNeeded];
if (!isGoingToBeRemoved) {
[self updateViewControllerIfNeeded];
}
}

- (void)replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubview *)childComponentView
Expand Down
Loading