From 47a9a9169fe0e6580a14a861d548996f54a067c4 Mon Sep 17 00:00:00 2001 From: Davide Callegaro <59137992+davidecallegaro@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:41:28 +0100 Subject: [PATCH] Ensure RCTScrollViewComponentView scroll view always matches its bounds The inner RCTEnhancedScrollView is created at CGRectZero and sized only by its autoresizing mask. Autoresizing propagates deltas at the moment the parent frame changes; if that moment is missed (heavy mount / teardown churn, reproducible on thermally-throttled devices), the scroll view stays zero-sized forever and fully-mounted, correctly-laid-out content is clipped into invisibility while ShadowTree layout events keep firing. Enforce the invariant explicitly in updateLayoutMetrics and layoutSubviews, preserving contentOffset (bounds.origin) and the RTL transform (bounds.size + center rather than frame). --- .../ScrollView/RCTScrollViewComponentView.mm | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 45c69b13f4b..4fe210bd468 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -315,6 +315,39 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics _containerView.transform = transform; _scrollView.transform = transform; } + [self _ensureScrollViewMatchesBounds]; +} + +// The scroll view is sized only by its autoresizing mask (set in init), which +// propagates deltas at the moment this view's frame changes. If a frame change +// lands while the delta cannot propagate (observed on-device: heavy mount / +// teardown churn under thermal throttle), the scroll view is left at +// CGRectZero permanently: fully mounted, correctly laid-out content is clipped +// into invisibility while ShadowTree layout events keep firing. Enforce the +// invariant explicitly so any desync self-heals on the next layout pass. +- (void)_ensureScrollViewMatchesBounds +{ + CGRect bounds = self.bounds; + if (CGSizeEqualToSize(bounds.size, CGSizeZero)) { + return; + } + CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); + if (!CGSizeEqualToSize(_scrollView.bounds.size, bounds.size) || + !CGPointEqualToPoint(_scrollView.center, center)) { + // Adjust bounds.size + center rather than frame so the RTL transform is + // respected, and preserve bounds.origin - for UIScrollView it is the + // contentOffset. + CGRect scrollViewBounds = _scrollView.bounds; + scrollViewBounds.size = bounds.size; + _scrollView.bounds = scrollViewBounds; + _scrollView.center = center; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self _ensureScrollViewMatchesBounds]; } - (bool)isInverted