From 47903d0c62de9399a5cd0993a59e4af0bdea4f0c Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Mon, 30 Jan 2023 11:10:51 -0800 Subject: [PATCH] Restore scroll position when scroll view is hidden and shown Summary: ScrollViews don't properly maintain position where they are hidden and shown. On iOS, when a UIScrollView (or its ancestor) is hidden, its scroll position is set to 0 (its window also becomes nil). When it is shown again, its scroll position is not restored. When a scroll is attempted when the scroll view is hidden, we keep track of the last known offset before it was hidden. Then, in updateLayoutMetrics (which is triggered when the view is shown), we apply the pending offset if there is one. This is [consistent with Android's behavior in ReactScrollView.java](https://www.internalfb.com/code/fbsource/[2930f8c146af62ad63673c8d34e9876b77634c05]/xplat/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java?lines=289). Changelog: [Internal][Fixed] - In onLayoutChange, only scroll if the view is shown and the content view is ready Reviewed By: cipolleschi Differential Revision: D42815359 fbshipit-source-id: 4b209c1e54edf3f5c0bea902b48450a1a2e9661a --- .../ScrollView/RCTScrollViewComponentView.mm | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 8c95cdbc0b9e18..ae41b9cb8dfd2b 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -25,6 +25,12 @@ using namespace facebook::react; +struct PendingOffset { + bool isPending; + CGPoint offset; + CGPoint lastOffset; +}; + static CGFloat const kClippingLeeway = 44.0; static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(ScrollViewProps const &props) @@ -99,6 +105,8 @@ @implementation RCTScrollViewComponentView { BOOL _shouldUpdateContentInsetAdjustmentBehavior; CGPoint _contentOffsetWhenClipped; + + PendingOffset _pendingOffset; } + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view @@ -173,6 +181,12 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics _containerView.transform = transform; _scrollView.transform = transform; } + + // If there is a pending offset, apply it + if (_pendingOffset.isPending) { + [self scrollTo:_pendingOffset.offset.x y:_pendingOffset.offset.y animated:false]; + _pendingOffset.isPending = false; + } } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps @@ -421,6 +435,13 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView [self _updateStateWithContentOffset]; } + // If the view is hidden, then set as pending offset. Apply it later on + // updateLayoutMetrics. + if (_scrollView.window == nil && !_pendingOffset.isPending) { + _pendingOffset.offset = _pendingOffset.lastOffset; + _pendingOffset.isPending = true; + } + NSTimeInterval now = CACurrentMediaTime(); if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) { _lastScrollEventDispatchTime = now; @@ -431,6 +452,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag); } + _pendingOffset.lastOffset = _scrollView.contentOffset; + [self _remountChildrenIfNeeded]; }