Skip to content

fix(iOS, Stack v4): iterate over a copy of reactSubviews when updating header shadow state - #4851

Merged
kmichalikk merged 2 commits into
software-mansion:mainfrom
ilp0:fix/4811-header-subview-enumeration
Oct 21, 2026
Merged

kmichalikk merged 2 commits into
software-mansion:mainfrom
ilp0:fix/4811-header-subview-enumeration

Conversation

@ilp0

@ilp0 ilp0 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

-[RNSScreenStackHeaderConfig updateHeaderStateInShadowTreeInContextOfNavigationBar:] loops over self.reactSubviews and updates the shadow state of each header subview. Since #3933 (4.19.0) that update is synchronous, so it can commit and mount right away, and the mount includes any React commit that's still pending. If that commit adds or removes a header subview, the array changes while we're still looping over it, and iOS throws NSGenericException: Collection was mutated while being enumerated.

Fast enumeration only detects the change when it moves on to the next element, so it only crashes when another header subview comes after the one being updated. In practice the item that commits is a left or right item that was just mounted. With native-stack, the only subview mounted after the right items is the search bar, so there it takes custom header items that change often plus headerSearchBarOptions. With ScreenStackItem the order of the subviews is up to the app, and any subview after the item is enough.

Closes #4811.

Changes

  • Loop over a copy of reactSubviews in updateHeaderStateInShadowTreeInContextOfNavigationBar:.
  • Add Test4811, which reproduces the crash.

I only changed this one loop on purpose. The other reactSubviews loops in the file (hitTest:withEvent:, hasSubviewOfType: and loadBackButtonImageInViewController:) only read values and don't call anything that can commit, so they can't hit this. Happy to change them too if you'd rather be defensive everywhere.

Copying the array is cheap, since a header only has a handful of subviews.

Test plan

Test4811: press "Start". The screen commits as fast as possible. The left and right header items are mounted and unmounted at different rates, and the title is placed after the right item.

Tested in FabricExample on main (iOS 26.5 simulator):

  • Without the fix: it crashed right after pressing "Start" in 5 out of 5 runs, with the same stack as in the issue.
  • With the fix: no crash in a 60-second run (about 4,500 updates), and the header items keep rendering correctly.

I also checked the native-stack case with a search bar on main: without the fix it crashed in 5 out of 5 runs within 6 seconds, with the fix there were no crashes in 3 runs of 60 seconds.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fc18d3b9-b187-4c39-bb24-fa6fbdea14c2
📥 Commits

Reviewing files that changed from the base of the PR and between 00194b1 and cbc4b52.

📒 Files selected for processing: 3
  • apps/src/tests/issue-tests/Test4811.tsx
  • apps/src/tests/issue-tests/index.ts
  • ios/legacy/RNSScreenStackHeaderConfig.mm

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

The iOS header update loop now enumerates a copy of its subviews. The issue-test app repeatedly changes header items during zero-delay updates and registers the reproduction screen.

Changes: Issue 4811 iOS header updates

Layer / File(s) Summary
Enumerate a copied subview array
ios/legacy/RNSScreenStackHeaderConfig.mm
The header shadow-state update loop iterates over a copy of reactSubviews.
Add the issue reproduction screen
apps/src/tests/issue-tests/Test4811.tsx, apps/src/tests/issue-tests/index.ts
The reproduction screen toggles left and right header items at different tick rates, keeps a center item mounted, and provides Start, Stop, and update-count controls. The issue-test index exports the screen.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Test4811
  participant NativeStack
  participant RNSScreenStackHeaderConfig
  Test4811->>NativeStack: Toggle header items during update ticks
  NativeStack->>RNSScreenStackHeaderConfig: Update header configuration
  RNSScreenStackHeaderConfig->>RNSScreenStackHeaderConfig: Enumerate copied reactSubviews
Loading

Suggested reviewers: loloekk

Merge Risk: ⚪ Minimal · up to cbc4b

No issue requiring a fix before merge is identified; the change addresses the reported header-update crash and adds a reproduction screen.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the iOS Stack v4 fix and the specific change to iterate over a copy of reactSubviews.
Description check Passed The description directly explains the crash, the fix, the reproduction test, and the reported validation results.
Linked Issues check Passed The change satisfies the coding requirement in [#4811]. ios/legacy/RNSScreenStackHeaderConfig.mm now iterates over [self.reactSubviews copy] in `updateHeaderStateInShadowTreeInContextOfNavigationB…
Out of Scope Changes check Passed The changes remain within [#4811]. The Objective-C++ change fixes the reported enumeration failure. Test4811 and its export provide a focused automated reproduction. No unrelated production behavior…
Full details: Linked Issues check

Explanation

The change satisfies the coding requirement in [#4811]. ios/legacy/RNSScreenStackHeaderConfig.mm now iterates over [self.reactSubviews copy] in updateHeaderStateInShadowTreeInContextOfNavigationBar:. This prevents synchronous shadow-state updates from mutating the enumerated array. Test4811 reproduces repeated header subview changes and is exported from the issue-test index. The other loops do not perform the mutating shadow-state update described by [#4811], so the focused change meets the issue requirement.

Full details: Out of Scope Changes check

Explanation

The changes remain within [#4811]. The Objective-C++ change fixes the reported enumeration failure. Test4811 and its export provide a focused automated reproduction. No unrelated production behavior or public API change is shown.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kmichalikk kmichalikk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ilp0
Looks good, thanks for the PR. Please rebase the PR and simplify the test a bit (at least remove the search controller, it threw me off for a moment).

ilp0 added 2 commits October 20, 2026 12:18
Use ScreenStackItem with the header subviews in an explicit order instead of
native-stack. The title after the right item is what makes the mutation visible
to fast enumeration, so the search bar is no longer needed.
@ilp0
ilp0 force-pushed the fix/4811-header-subview-enumeration branch from 00194b1 to cbc4b52 Compare October 20, 2026 11:53
@ilp0

ilp0 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Hi @kmichalikk, thanks for the review. I rebased onto main and simplified the test

The test now uses ScreenStackItem directly with the title after the right item, and there’s no search bar anymore. I used the search bar as an example, because this is how I caught the bug in the first place :)

On main in FabricExample it crashed right after pressing “Start” in 5 out of 5 runs without the fix, and didn’t crash with it.

@kmichalikk kmichalikk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on physical device with iOS 26.6.1, too

didn't trigger on 27 for me
still, worth fixing for 26

@kmichalikk
kmichalikk merged commit 979b511 into software-mansion:main Oct 21, 2026
8 of 10 checks passed
@kmichalikk kmichalikk added the action:backport-to-v4 Add this label to any issue or PR that should be backported to the v4 line of the library. label Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action:backport-to-v4 Add this label to any issue or PR that should be backported to the v4 line of the library.

Projects

None yet

2 participants