Skip to content
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
95 changes: 95 additions & 0 deletions apps/src/tests/issue-tests/Test4811.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useEffect, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import {
ScreenStack,
ScreenStackHeaderCenterView,
ScreenStackHeaderLeftView,
ScreenStackHeaderRightView,
ScreenStackItem,
} from 'react-native-screens';

// https://github.com/software-mansion/react-native-screens/issues/4811 (iOS)
//
// Press "Start". Without the fix the app crashes within a few seconds with
// "Collection <__NSArrayM> was mutated while being enumerated".
//
// `updateHeaderStateInShadowTreeInContextOfNavigationBar:` loops over `reactSubviews`.
// A header item that was just mounted sends its frame to the shadow tree synchronously,
// and that can mount the next pending commit. Here the left and right items toggle at
// different rates, so that commit adds or removes a header subview during the loop.
// Fast enumeration only notices it when it moves on to the next element, so the title
// is placed after the right item.

function HeaderItem() {
return <View style={styles.item} />;
}

export default function App() {
const [running, setRunning] = useState(false);
const [tick, setTick] = useState(0);

useEffect(() => {
if (!running) {
return;
}
let timer: ReturnType<typeof setTimeout>;
const loop = () => {
setTick(t => t + 1);
timer = setTimeout(loop, 0);
};
loop();
return () => clearTimeout(timer);
}, [running]);

return (
<ScreenStack style={styles.stack}>
<ScreenStackItem
screenId="home"
headerConfig={{
children: (
<>
{tick % 2 === 0 && (
<ScreenStackHeaderLeftView>
<HeaderItem />
</ScreenStackHeaderLeftView>
)}
{tick % 3 !== 0 && (
<ScreenStackHeaderRightView>
<HeaderItem />
</ScreenStackHeaderRightView>
)}
<ScreenStackHeaderCenterView>
<HeaderItem />
</ScreenStackHeaderCenterView>
</>
),
}}>
<View style={styles.container}>
<Button
title={running ? 'Stop' : 'Start'}
onPress={() => setRunning(r => !r)}
/>
<Text>Updates: {tick}</Text>
</View>
</ScreenStackItem>
</ScreenStack>
);
}

const styles = StyleSheet.create({
stack: {
flex: 1,
},
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 16,
},
item: {
width: 40,
height: 28,
borderRadius: 6,
backgroundColor: '#f2b8b5',
},
});
1 change: 1 addition & 0 deletions apps/src/tests/issue-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export { default as Test4618 } from './Test4618'; // [E2E skipped]: tvOS Menu ke
export { default as Test4709 } from './Test4709';
export { default as Test4725 } from './Test4725';
export { default as Test4757 } from './Test4757';
export { default as Test4811 } from './Test4811';
export { default as Test4872 } from './Test4872';
export { default as TestScreenAnimation } from './TestScreenAnimation';
// The following test was meant to demo the "go back" gesture using Reanimated
Expand Down
5 changes: 4 additions & 1 deletion ios/legacy/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,10 @@ - (void)updateHeaderStateInShadowTreeInContextOfNavigationBar:(nullable UINaviga
[self updateShadowStateWithSize:navigationBar.frame.size
edgeInsets:[self computeEdgeInsetsOfNavigationBar:navigationBar]
frameOrigin:navBarFrameInScreenView.origin];
for (RNSScreenStackHeaderSubview *subview in self.reactSubviews) {
// Iterate over a copy. Updating a subview's shadow state can trigger a synchronous commit that mounts or
// unmounts header subviews, which mutates `reactSubviews` while we're still enumerating it.
// See: https://github.com/software-mansion/react-native-screens/issues/4811
for (RNSScreenStackHeaderSubview *subview in [self.reactSubviews copy]) {
[subview updateShadowStateInContextOfAncestorView:navigationBar];
}
}
Expand Down
Loading