Skip to content

Commit

Permalink
fix(iOS): header snapshots not working (#2393)
Browse files Browse the repository at this point in the history
## Description

The `updateViewControllerIfNeeded` call introduced by
#2230
forces the view controller to rebuild the subviews with the recent
config.
When the screen is being unmounted it replaces the subviews with nil
values as the `reactSubviews` are removed from the config one by one.
Snapshots made earlier get discarded in the process.

This PR adds a condition that prevents updating the view controller when
the screen is being changed + stops unnecessary snapshots when the
screen is not changed.

## Changes

- updated `Test556.tsx` repro
- added `isGoingToBeRemoved` property to `RNSScreenView`
- making snapshots / updating the view controller conditionally

 <!--
## Screenshots / GIFs
-->

## Test code and steps to reproduce

- Use `Test556.tsx` repro

## 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>
Co-authored-by: Kacper Kafara <kacperkafara@gmail.com>
  • Loading branch information
3 people authored Oct 15, 2024
1 parent d40e108 commit e4333a1
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 34 deletions.
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

0 comments on commit e4333a1

Please sign in to comment.