From dcd0841da38d447e1a4c31ddcde5340671e2addc Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 12 Sep 2026 21:56:02 +0000 Subject: [PATCH] fix(ios): keep a zoomed ScrollView usable when its content size changes `RCTScrollViewComponentView`'s `_containerView` is also the view that UIScrollView zooms (`viewForZoomingInScrollView:`), so while the user is pinch-zoomed in it carries a scale transform. On every content size change `updateState:oldState:` assigned `_containerView.frame`, which is undefined behavior for a view with a non-identity transform (UIKit ends up shrinking the view's bounds by the zoom scale), and it also passed the unzoomed size to the UIScrollView as `contentSize`. The visible result: pinch-zoom into a ScrollView, then change its size (rotate the device, or change the layout around it). The scrolled position jumps, and the content can no longer be zoomed all the way out; it stays stuck in a corner until the user zooms out and resizes again. Lay the container out through `bounds` and `center` instead, and give the scroll view the zoomed content size, which is what UIScrollView itself keeps `contentSize` at while zooming. --- .../ScrollView/RCTScrollViewComponentView.mm | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 14a08b49cb7c..b38bc481cb95 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -495,10 +495,19 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared & } _contentSize = contentSize; - _containerView.frame = CGRect{RCTCGPointFromPoint(data.contentBoundingRect.origin), contentSize}; + + // `_containerView` is the scroll view's zooming view (see `viewForZoomingInScrollView:`), so while the user is + // pinch-zoomed in it carries a scale transform. Setting `frame` on a view whose `transform` is not the identity is + // undefined behavior, so lay it out through `bounds` and `center` instead. The scroll view must get the *zoomed* + // size, which is what UIScrollView itself keeps `contentSize` at while zooming. + CGPoint contentOrigin = RCTCGPointFromPoint(data.contentBoundingRect.origin); + _containerView.bounds = CGRect{CGPointZero, contentSize}; + CGSize zoomedContentSize = _containerView.frame.size; + _containerView.center = + CGPoint{contentOrigin.x + zoomedContentSize.width / 2, contentOrigin.y + zoomedContentSize.height / 2}; [self _preserveContentOffsetIfNeededWithBlock:^{ - self->_scrollView.contentSize = contentSize; + self->_scrollView.contentSize = zoomedContentSize; }]; }