Skip to content

Commit 639122b

Browse files
tbobakkafar
andauthored
fix(iOS): incorrect safe area on transparent modals using landscape orientation (software-mansion#2008)
## Description Normally, while opening a modal, each screen should preserve its safe area, depending on the screen orientation. Unfortunately, when user dismisses transparent modals, safe area of the origin screen breaks, resulting in the vertical safe area on landscape orientation. Because of that, I investigated the logic behind the final decision of screen orientations and it looks like the problem was lying on `supportedScreenOrientations` method in RNSScreen.mm file. 1. First, the modal was being asked for its supported device orientation. Once it reached the `supportedScreenOrientations` method, it was asking for config of childVC. Because the childVC was `nil` and the orientation wasn't set in screen options, it was returning `nil`, resulting in returning all orientations without `upside down`. 2. After that, there was a time for looking onto child VCs of a screen behind the modal. Since it was presenting a modal, it was first checking for its last child - since it didn't provide any modal and it haven't got any children, the last child was also `nil`, resulting in returning the modal as an orientation of the screen. 3. Returning a modal is (probably) a bad idea here, since it does not have any screen orientation specified, resulting in returning `nil` as a screen orientation. This was probably resulting a bug with wrong safe area. This PR changes this bad behavior by not returning `lastViewController` (which is a modal that is being presented from a screen) and going further for looking a config in child view controllers. However, this behavior may lead to the further bugs we haven't discovered yet. ## Changes - Changed `return` statement in `findChildVCForConfigAndTrait` method. ## Screenshots / GIFs ### Before https://github.com/software-mansion/react-native-screens/assets/23281839/6ff888d9-a7df-466c-ac45-d3db1cbe928c ### After https://github.com/software-mansion/react-native-screens/assets/23281839/dfc03a6c-ca7a-49a0-9c3e-7f019f7d405c ## Test code and steps to reproduce You can check Test2008 in `TestsExample` and `FabricTestExample` in order to tests how transparent modals behave. ## 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>
1 parent 88d72a9 commit 639122b

File tree

5 files changed

+387
-4
lines changed

5 files changed

+387
-4
lines changed

FabricTestExample/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import Test1844 from './src/Test1844';
9292
import Test1864 from './src/Test1864';
9393
import TestScreenAnimation from './src/TestScreenAnimation';
9494
import Test1981 from './src/Test1981';
95+
import Test2008 from './src/Test2008';
9596

9697
enableFreeze(true);
9798

FabricTestExample/src/Test2008.tsx

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import React from 'react';
2+
import { Text, View, SafeAreaView, StyleSheet, Pressable } from 'react-native';
3+
import {
4+
createNativeStackNavigator,
5+
NativeStackNavigationProp,
6+
} from '@react-navigation/native-stack';
7+
import {
8+
NavigationContainer,
9+
useNavigation,
10+
ParamListBase,
11+
type NavigationProp,
12+
} from '@react-navigation/native';
13+
import { SafeAreaProvider } from 'react-native-safe-area-context';
14+
15+
const styles = StyleSheet.create({
16+
inner: {
17+
flex: 1,
18+
backgroundColor: 'white',
19+
},
20+
safeArea: {
21+
flex: 1,
22+
backgroundColor: 'red',
23+
},
24+
innerModal: {
25+
flex: 1,
26+
backgroundColor: 'rgba(0,0,0,0.4)',
27+
justifyContent: 'center',
28+
alignItems: 'center',
29+
},
30+
pressable: {
31+
padding: 20,
32+
backgroundColor: '#ccc',
33+
marginVertical: 5,
34+
},
35+
36+
textIntro: {
37+
padding: 10,
38+
},
39+
40+
buttons: {
41+
flexDirection: 'row',
42+
padding: 10,
43+
},
44+
45+
text: {
46+
textAlign: 'center',
47+
},
48+
});
49+
50+
type RootStackScreens = {
51+
Home: undefined;
52+
Modal: undefined;
53+
TransparentModal: undefined;
54+
ContainedTransparentModal: undefined;
55+
};
56+
57+
const RootStack = createNativeStackNavigator<RootStackScreens>();
58+
59+
function Home() {
60+
const navigation = useNavigation<NavigationProp<RootStackScreens>>();
61+
return (
62+
<SafeAreaView style={styles.safeArea}>
63+
<View style={styles.inner}>
64+
<Text style={styles.textIntro}>
65+
Red represents the safe area padding as provided by React Native Safe
66+
Area Context (although I've noticed that the issue also affects the
67+
build in react native SafeArea component).
68+
</Text>
69+
<Text style={styles.textIntro}>
70+
This only applies to iOS. Ensure you have rotation lock off, and
71+
rotate the view into landscape orientation. Note how the red safe
72+
areas appear. Then tap `Spawn Transparent Modal`, dismiss the modal,
73+
and then rotate the screen again to see how the safe areas are now
74+
stuck as the portrait values. You must force quite the app to undo the
75+
bug.
76+
</Text>
77+
<Pressable
78+
style={styles.pressable}
79+
onPress={() => navigation.navigate('Modal')}>
80+
<Text style={styles.text}>"modal"</Text>
81+
</Pressable>
82+
<Pressable
83+
style={styles.pressable}
84+
onPress={() => navigation.navigate('ContainedTransparentModal')}>
85+
<Text style={styles.text}>"containedTransparentModal"</Text>
86+
</Pressable>
87+
<Pressable
88+
style={styles.pressable}
89+
onPress={() => navigation.navigate('TransparentModal')}>
90+
<Text style={styles.text}>"transparentModal"</Text>
91+
</Pressable>
92+
</View>
93+
</SafeAreaView>
94+
);
95+
}
96+
97+
function Modal({
98+
navigation,
99+
}: {
100+
navigation: NativeStackNavigationProp<ParamListBase>;
101+
}) {
102+
return (
103+
<View style={styles.innerModal}>
104+
<Text>Modal</Text>
105+
<Pressable style={styles.pressable} onPress={() => navigation.goBack()}>
106+
<Text>Go Back!</Text>
107+
</Pressable>
108+
<Pressable
109+
style={styles.pressable}
110+
onPress={() => navigation.push('Modal')}>
111+
<Text>Open another modal!</Text>
112+
</Pressable>
113+
</View>
114+
);
115+
}
116+
117+
function TransparentModal({
118+
navigation,
119+
}: {
120+
navigation: NativeStackNavigationProp<ParamListBase>;
121+
}) {
122+
return (
123+
<View style={styles.innerModal}>
124+
<Text>Transparent Modal</Text>
125+
<Pressable style={styles.pressable} onPress={() => navigation.goBack()}>
126+
<Text>Go Back!</Text>
127+
</Pressable>
128+
<Pressable
129+
style={styles.pressable}
130+
onPress={() => navigation.push('TransparentModal')}>
131+
<Text>Open another modal!</Text>
132+
</Pressable>
133+
</View>
134+
);
135+
}
136+
137+
function ContainedTransparentModal({
138+
navigation,
139+
}: {
140+
navigation: NativeStackNavigationProp<ParamListBase>;
141+
}) {
142+
return (
143+
<View style={styles.innerModal}>
144+
<Text>Contained Transparent Modal</Text>
145+
<Pressable style={styles.pressable} onPress={() => navigation.goBack()}>
146+
<Text>Go Back!</Text>
147+
</Pressable>
148+
<Pressable
149+
style={styles.pressable}
150+
onPress={() => navigation.push('ContainedTransparentModal')}>
151+
<Text>Open another modal!</Text>
152+
</Pressable>
153+
</View>
154+
);
155+
}
156+
157+
export default function App() {
158+
return (
159+
<SafeAreaProvider>
160+
<NavigationContainer>
161+
<RootStack.Navigator
162+
initialRouteName="Home"
163+
screenOptions={{ headerShown: false }}>
164+
<RootStack.Screen name="Home" component={Home} />
165+
<RootStack.Screen
166+
name="TransparentModal"
167+
component={TransparentModal}
168+
options={{ presentation: 'transparentModal' }}
169+
/>
170+
<RootStack.Screen
171+
name="ContainedTransparentModal"
172+
component={ContainedTransparentModal}
173+
options={{ presentation: 'containedTransparentModal' }}
174+
/>
175+
<RootStack.Screen
176+
name="Modal"
177+
component={Modal}
178+
options={{ presentation: 'modal' }}
179+
/>
180+
</RootStack.Navigator>
181+
</NavigationContainer>
182+
</SafeAreaProvider>
183+
);
184+
}

TestsExample/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import Test1829 from './src/Test1829';
9494
import Test1844 from './src/Test1844';
9595
import Test1864 from './src/Test1864';
9696
import Test1981 from './src/Test1981';
97+
import Test2008 from './src/Test2008';
9798

9899
enableFreeze(true);
99100

TestsExample/src/Test2008.tsx

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import React from 'react';
2+
import { Text, View, SafeAreaView, StyleSheet, Pressable } from 'react-native';
3+
import {
4+
createNativeStackNavigator,
5+
NativeStackNavigationProp,
6+
} from '@react-navigation/native-stack';
7+
import {
8+
NavigationContainer,
9+
useNavigation,
10+
ParamListBase,
11+
type NavigationProp,
12+
} from '@react-navigation/native';
13+
import { SafeAreaProvider } from 'react-native-safe-area-context';
14+
15+
const styles = StyleSheet.create({
16+
inner: {
17+
flex: 1,
18+
backgroundColor: 'white',
19+
},
20+
safeArea: {
21+
flex: 1,
22+
backgroundColor: 'red',
23+
},
24+
innerModal: {
25+
flex: 1,
26+
backgroundColor: 'rgba(0,0,0,0.4)',
27+
justifyContent: 'center',
28+
alignItems: 'center',
29+
},
30+
pressable: {
31+
padding: 20,
32+
backgroundColor: '#ccc',
33+
marginVertical: 5,
34+
},
35+
36+
textIntro: {
37+
padding: 10,
38+
},
39+
40+
buttons: {
41+
flexDirection: 'row',
42+
padding: 10,
43+
},
44+
45+
text: {
46+
textAlign: 'center',
47+
},
48+
});
49+
50+
type RootStackScreens = {
51+
Home: undefined;
52+
Modal: undefined;
53+
TransparentModal: undefined;
54+
ContainedTransparentModal: undefined;
55+
};
56+
57+
const RootStack = createNativeStackNavigator<RootStackScreens>();
58+
59+
function Home() {
60+
const navigation = useNavigation<NavigationProp<RootStackScreens>>();
61+
return (
62+
<SafeAreaView style={styles.safeArea}>
63+
<View style={styles.inner}>
64+
<Text style={styles.textIntro}>
65+
Red represents the safe area padding as provided by React Native Safe
66+
Area Context (although I've noticed that the issue also affects the
67+
build in react native SafeArea component).
68+
</Text>
69+
<Text style={styles.textIntro}>
70+
This only applies to iOS. Ensure you have rotation lock off, and
71+
rotate the view into landscape orientation. Note how the red safe
72+
areas appear. Then tap `Spawn Transparent Modal`, dismiss the modal,
73+
and then rotate the screen again to see how the safe areas are now
74+
stuck as the portrait values. You must force quite the app to undo the
75+
bug.
76+
</Text>
77+
<Pressable
78+
style={styles.pressable}
79+
onPress={() => navigation.navigate('Modal')}>
80+
<Text style={styles.text}>"modal"</Text>
81+
</Pressable>
82+
<Pressable
83+
style={styles.pressable}
84+
onPress={() => navigation.navigate('ContainedTransparentModal')}>
85+
<Text style={styles.text}>"containedTransparentModal"</Text>
86+
</Pressable>
87+
<Pressable
88+
style={styles.pressable}
89+
onPress={() => navigation.navigate('TransparentModal')}>
90+
<Text style={styles.text}>"transparentModal"</Text>
91+
</Pressable>
92+
</View>
93+
</SafeAreaView>
94+
);
95+
}
96+
97+
function Modal({
98+
navigation,
99+
}: {
100+
navigation: NativeStackNavigationProp<ParamListBase>;
101+
}) {
102+
return (
103+
<View style={styles.innerModal}>
104+
<Text>Modal</Text>
105+
<Pressable style={styles.pressable} onPress={() => navigation.goBack()}>
106+
<Text>Go Back!</Text>
107+
</Pressable>
108+
<Pressable
109+
style={styles.pressable}
110+
onPress={() => navigation.push('Modal')}>
111+
<Text>Open another modal!</Text>
112+
</Pressable>
113+
</View>
114+
);
115+
}
116+
117+
function TransparentModal({
118+
navigation,
119+
}: {
120+
navigation: NativeStackNavigationProp<ParamListBase>;
121+
}) {
122+
return (
123+
<View style={styles.innerModal}>
124+
<Text>Transparent Modal</Text>
125+
<Pressable style={styles.pressable} onPress={() => navigation.goBack()}>
126+
<Text>Go Back!</Text>
127+
</Pressable>
128+
<Pressable
129+
style={styles.pressable}
130+
onPress={() => navigation.push('TransparentModal')}>
131+
<Text>Open another modal!</Text>
132+
</Pressable>
133+
</View>
134+
);
135+
}
136+
137+
function ContainedTransparentModal({
138+
navigation,
139+
}: {
140+
navigation: NativeStackNavigationProp<ParamListBase>;
141+
}) {
142+
return (
143+
<View style={styles.innerModal}>
144+
<Text>Contained Transparent Modal</Text>
145+
<Pressable style={styles.pressable} onPress={() => navigation.goBack()}>
146+
<Text>Go Back!</Text>
147+
</Pressable>
148+
<Pressable
149+
style={styles.pressable}
150+
onPress={() => navigation.push('ContainedTransparentModal')}>
151+
<Text>Open another modal!</Text>
152+
</Pressable>
153+
</View>
154+
);
155+
}
156+
157+
export default function App() {
158+
return (
159+
<SafeAreaProvider>
160+
<NavigationContainer>
161+
<RootStack.Navigator
162+
initialRouteName="Home"
163+
screenOptions={{ headerShown: false }}>
164+
<RootStack.Screen name="Home" component={Home} />
165+
<RootStack.Screen
166+
name="TransparentModal"
167+
component={TransparentModal}
168+
options={{ presentation: 'transparentModal' }}
169+
/>
170+
<RootStack.Screen
171+
name="ContainedTransparentModal"
172+
component={ContainedTransparentModal}
173+
options={{ presentation: 'containedTransparentModal' }}
174+
/>
175+
<RootStack.Screen
176+
name="Modal"
177+
component={Modal}
178+
options={{ presentation: 'modal' }}
179+
/>
180+
</RootStack.Navigator>
181+
</NavigationContainer>
182+
</SafeAreaProvider>
183+
);
184+
}

0 commit comments

Comments
 (0)