diff --git a/apps/src/tests/issue-tests/Test4811.tsx b/apps/src/tests/issue-tests/Test4811.tsx
new file mode 100644
index 0000000000..e1e96ff1cf
--- /dev/null
+++ b/apps/src/tests/issue-tests/Test4811.tsx
@@ -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 ;
+}
+
+export default function App() {
+ const [running, setRunning] = useState(false);
+ const [tick, setTick] = useState(0);
+
+ useEffect(() => {
+ if (!running) {
+ return;
+ }
+ let timer: ReturnType;
+ const loop = () => {
+ setTick(t => t + 1);
+ timer = setTimeout(loop, 0);
+ };
+ loop();
+ return () => clearTimeout(timer);
+ }, [running]);
+
+ return (
+
+
+ {tick % 2 === 0 && (
+
+
+
+ )}
+ {tick % 3 !== 0 && (
+
+
+
+ )}
+
+
+
+ >
+ ),
+ }}>
+
+
+
+
+ );
+}
+
+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',
+ },
+});
diff --git a/apps/src/tests/issue-tests/index.ts b/apps/src/tests/issue-tests/index.ts
index 84586c2cad..893215a1a6 100644
--- a/apps/src/tests/issue-tests/index.ts
+++ b/apps/src/tests/issue-tests/index.ts
@@ -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
diff --git a/ios/legacy/RNSScreenStackHeaderConfig.mm b/ios/legacy/RNSScreenStackHeaderConfig.mm
index 991b2961d0..e5e167ee69 100644
--- a/ios/legacy/RNSScreenStackHeaderConfig.mm
+++ b/ios/legacy/RNSScreenStackHeaderConfig.mm
@@ -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];
}
}