Skip to content

Commit bb86f5b

Browse files
alduzykkafar
andcommitted
fix(iOS): header snapshots not working (#2393)
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. - updated `Test556.tsx` repro - added `isGoingToBeRemoved` property to `RNSScreenView` - making snapshots / updating the view controller conditionally <!-- --> - Use `Test556.tsx` repro - [x] Included code example that can be used to test this change - [x] Ensured that CI passes --------- Co-authored-by: Kacper Kafara <[email protected]> Co-authored-by: Kacper Kafara <[email protected]> (cherry picked from commit e4333a1)
1 parent 5a9afbb commit bb86f5b

File tree

5 files changed

+99
-34
lines changed

5 files changed

+99
-34
lines changed

apps/src/tests/Test556.tsx

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import * as React from 'react';
22
import { Button, StyleSheet, Text, View } from 'react-native';
33
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
4-
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack';
4+
import {
5+
NativeStackNavigationProp,
6+
createNativeStackNavigator,
7+
} from '@react-navigation/native-stack';
8+
import { Square } from '../shared';
59

610
const Stack = createNativeStackNavigator();
711

812
type ScreenBaseProps = {
913
navigation: NativeStackNavigationProp<ParamListBase>;
10-
}
14+
};
1115

1216
export default function App() {
1317
return (
@@ -16,30 +20,48 @@ export default function App() {
1620
screenOptions={{
1721
animation: 'fade',
1822
}}>
19-
<Stack.Screen name="First" component={First} options={{
20-
headerTitle: () => (
21-
<View style={[styles.container, { backgroundColor: 'goldenrod' }]}>
22-
<Text>Hello there!</Text>
23-
</View>
24-
),
25-
headerRight: () => (
26-
<View style={[styles.container, { backgroundColor: 'lightblue' }]}>
27-
<Text>Right-1</Text>
28-
</View>
29-
),
30-
}} />
31-
<Stack.Screen name="Second" component={Second} options={{
32-
headerTitle: () => (
33-
<View style={[styles.container, { backgroundColor: 'mediumseagreen' }]}>
34-
<Text>General Kenobi</Text>
35-
</View>
36-
),
37-
headerRight: () => (
38-
<View style={[styles.container, { backgroundColor: 'mediumvioletred' }]}>
39-
<Text>Right-2</Text>
40-
</View>
41-
),
42-
}} />
23+
<Stack.Screen
24+
name="First"
25+
component={First}
26+
options={{
27+
headerTitle: () => (
28+
<View
29+
style={[styles.container, { backgroundColor: 'goldenrod' }]}>
30+
<Text>Hello there!</Text>
31+
</View>
32+
),
33+
headerRight: () => (
34+
<View
35+
style={[styles.container, { backgroundColor: 'lightblue' }]}>
36+
<Text>Right-1</Text>
37+
</View>
38+
),
39+
}}
40+
/>
41+
<Stack.Screen
42+
name="Second"
43+
component={Second}
44+
options={{
45+
headerTitle: () => (
46+
<View
47+
style={[
48+
styles.container,
49+
{ backgroundColor: 'mediumseagreen' },
50+
]}>
51+
<Text>General Kenobi</Text>
52+
</View>
53+
),
54+
headerRight: () => (
55+
<View
56+
style={[
57+
styles.container,
58+
{ backgroundColor: 'mediumvioletred' },
59+
]}>
60+
<Text>Right-2</Text>
61+
</View>
62+
),
63+
}}
64+
/>
4365
</Stack.Navigator>
4466
</NavigationContainer>
4567
);
@@ -55,11 +77,34 @@ function First({ navigation }: ScreenBaseProps) {
5577
}
5678

5779
function Second({ navigation }: ScreenBaseProps) {
80+
const [backButtonVisible, setBackButtonVisible] = React.useState(true);
81+
5882
return (
59-
<Button
60-
title="Tap me for first screen"
61-
onPress={() => navigation.popTo('First')}
62-
/>
83+
<>
84+
<Button
85+
title="Toggle left subview"
86+
onPress={() => {
87+
setBackButtonVisible(prev => !prev);
88+
navigation.setOptions({
89+
headerLeft: backButtonVisible
90+
? () => (
91+
<View
92+
style={[
93+
styles.container,
94+
{ backgroundColor: 'mediumblue' },
95+
]}>
96+
<Text>Left</Text>
97+
</View>
98+
)
99+
: undefined,
100+
});
101+
}}
102+
/>
103+
<Button
104+
title="Tap me for first screen"
105+
onPress={() => navigation.popTo('First')}
106+
/>
107+
</>
63108
);
64109
}
65110

ios/RNSScreen.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ namespace react = facebook::react;
9999
@property (nonatomic) react::LayoutMetrics newLayoutMetrics;
100100
@property (weak, nonatomic) RNSScreenStackHeaderConfig *config;
101101
@property (nonatomic, readonly) BOOL hasHeaderConfig;
102+
@property (nonatomic, readonly, getter=isMarkedForUnmountInCurrentTransaction)
103+
BOOL markedForUnmountInCurrentTransaction;
102104
#else
103105
@property (nonatomic, copy) RCTDirectEventBlock onAppear;
104106
@property (nonatomic, copy) RCTDirectEventBlock onDisappear;
@@ -122,6 +124,10 @@ namespace react = facebook::react;
122124
- (void)updateBounds;
123125
- (void)notifyDismissedWithCount:(int)dismissCount;
124126
- (instancetype)initWithFrame:(CGRect)frame;
127+
/// Tell `Screen` that it will be unmounted in next transaction.
128+
/// The component needs this so that we can later decide whether to
129+
/// replace it with snapshot or not.
130+
- (void)willBeUnmountedInUpcomingTransaction;
125131
#else
126132
- (instancetype)initWithBridge:(RCTBridge *)bridge;
127133
#endif // RCT_NEW_ARCH_ENABLED

ios/RNSScreen.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ - (void)initCommonProps
9898
#if !TARGET_OS_TV
9999
_sheetExpandsWhenScrolledToEdge = YES;
100100
#endif // !TARGET_OS_TV
101+
#ifdef RCT_NEW_ARCH_ENABLED
102+
_markedForUnmountInCurrentTransaction = NO;
103+
#endif // RCT_NEW_ARCH_ENABLED
101104
}
102105

103106
- (UIViewController *)reactViewController
@@ -690,6 +693,11 @@ - (BOOL)hasHeaderConfig
690693
return _config != nil;
691694
}
692695

696+
- (void)willBeUnmountedInUpcomingTransaction
697+
{
698+
_markedForUnmountInCurrentTransaction = YES;
699+
}
700+
693701
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
694702
{
695703
return react::concreteComponentDescriptorProvider<react::RNSScreenComponentDescriptor>();

ios/RNSScreenStack.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,7 @@ - (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction
11751175
if (mutation.type == react::ShadowViewMutation::Delete) {
11761176
RNSScreenView *_Nullable toBeRemovedChild = [self childScreenForTag:mutation.oldChildShadowView.tag];
11771177
if (toBeRemovedChild != nil) {
1178+
[toBeRemovedChild willBeUnmountedInUpcomingTransaction];
11781179
_toBeDeletedScreens.push_back(toBeRemovedChild);
11791180
}
11801181
}

ios/RNSScreenStackHeaderConfig.mm

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -768,12 +768,17 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
768768

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

779784
- (void)replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubview *)childComponentView

0 commit comments

Comments
 (0)