fix(iOS, Tabs): make bottom accessory content switching robust and survive window detach#4152
Open
sgup wants to merge 1 commit into
Open
Conversation
…rvive window detach Three failure modes in the dual-content-view switching workaround: 1. RCTViewComponentView.invalidateLayer unconditionally resets layer.opacity to the React props value on arbitrary commits, re-showing the off-environment content copy. Since that copy is mounted absoluteFill on top of the active one, it also captured all hit-testing, making the accessory unresponsive to taps. 2. The UITraitTabAccessoryEnvironment registration callback is occasionally missed during the minimize transition, leaving the wrong copy visible until the next trait change. 3. didMoveToWindow invalidates and nils the helper + shadow proxy whenever the accessory leaves the window (any full-screen push covering the tab bar), but never recreated them on re-entry — so all callbacks and content switching stayed permanently dead afterwards. Fixes: - Use hidden as the sole carrier of invisibility, with alpha normalized to 1.0 on both copies: invalidateLayer never touches hidden, and the opacity reset becomes a no-op by construction. Hidden views are also excluded from hit-testing. Apply to whichever content views are registered instead of requiring both. - Re-evaluate visibility from notifyWrapperViewFrameHasChanged (the existing KVO on the wrapper's center), so a missed trait callback self-corrects on the next frame change. - Recreate the helper + shadow proxy in didMoveToWindow on window re-entry, with a lighter window-leave teardown that preserves the Fabric state (only React can re-deliver it via updateState; true unmount still fully invalidates via RNSTabsHostComponentView). Fixes software-mansion#3798
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: The code / PR is made by Claude Fable, however I've manually tested and confirmed with my app on simulator and device.
Description
The bottom accessory's dual-content-view switching (the "content view switching workaround") has three failure modes we root-caused on a physical iOS 26 device in a production app (RN 0.85, new arch):
RCTViewComponentView.invalidateLayerresetslayer.opacityto the React props value on arbitrary commits, re-showing the off-environment copy (the bug reported in Bottom accessory content view opacity glitch: invalidateLayer resets visibility #3798). Because that copy is mountedabsoluteFillas the later sibling, it sits on top of the active copy and also captures all hit-testing — every tap on the accessory goes dead, not just "wrong content shown".UITraitTabAccessoryEnvironmentregistration callback is occasionally missed during the minimize transition, leaving the wrong copy visible until the next trait change.didMoveToWindowinvalidates and nils_helper+_shadowStateProxywhen the accessory leaves the window, but nothing recreated them on re-entry —registerForAccessoryFrameChangeswas messaged atnil, so all callbacks and content switching stayed dead and the accessory froze on whichever copy was last visible. This likely explains the "full-screen modal dismissal also causes bottom accessory glitches" report in the Bottom accessory content view opacity glitch: invalidateLayer resets visibility #3798 thread.Closes #3798.
Changes
RNSTabsBottomAccessoryHelper:handleContentViewVisibilityForEnvironmentIfNeedednow useshiddenas the sole carrier of invisibility, with alpha normalized to 1.0 on both copies.invalidateLayernever toucheshidden, and with alpha kept at 1.0 its opacity reset becomes a no-op by construction (no reliance on the privateinvalidateLayer, no KVO loops). Hidden views are also excluded from hit-testing, fixing the dead-taps half. Applied to whichever content views are registered (nil messages are no-ops) — the both-registered early-return previously left a lone registered view fully visible.notifyWrapperViewFrameHasChangedre-evaluates visibility: the wrapper-frame KVO fires reliably on every regular↔inline move (and once at registration viaNSKeyValueObservingOptionInitial), so a missed trait callback self-corrects.isContentViewSwitchingWorkaroundActive.RNSTabsBottomAccessoryComponentView.didMoveToWindow:_state— only React can re-deliver it (updateStateon a commit), so resetting it on a mere detach left the recreated shadow proxy unable to publish accessory frame updates. True unmount still fully invalidates viaRNSTabsHostComponentView.Test plan
Tested in a production Expo SDK 56 app (RN 0.85, new arch, Fabric) on a physical iOS 26 device, with these changes applied to 4.25.2 via package patching (identical code paths — this area is unchanged between 4.25.2 and
mainapart from the removed RN < 0.82 guards):Reproduction shape for the original bugs: a
NativeTabs(expo-router) app with aBottomAccessorywhose content differs per placement, scroll-minimizing tab bars, and any full-screen push — see #3798 for the opacity repro.Checklist