From b98b9f1fa7717283f368eb182a51d971b8776c80 Mon Sep 17 00:00:00 2001 From: shubhamguptadream11 Date: Fri, 13 Sep 2024 06:33:30 -0700 Subject: [PATCH] fix(iOS): fire onMomentumScrollEnd when UIScrollView is removed from window (#46277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Solves this issue: https://github.com/facebook/react-native/issues/46276 ## Changelog: [IOS] [ADDED] - fire onMomentumScrollEnd when UIScrollView is removed from window **Why the issue is happening?** The `onMomentumScrollEnd` event is typically triggered by the `UIScrollView` delegate methods `scrollViewDidEndDecelerating` and `scrollViewDidEndScrollingAnimation`. However, if the scroll view is removed from the window while navigating away, these delegate methods are not called, resulting in the event not being dispatched. This behaviour was particularly problematic in scenarios where a scroll view is in motion, and the user navigates away from the screen before the scrolling completes. In such cases, the `onMomentumScrollEnd` event would never fire, which further make scroll area un touchable or un responsive. **What we changed?** In the didMoveToWindow method, we added logic to handle the scenario where the UIScrollView is being removed from the window (i.e., when the component is unmounted or the user navigates away). Here’s a breakdown of the changes: - **Added a Check for Scroll State:** We check if the UIScrollView was decelerating or had stopped tracking (_scrollView.isDecelerating || _scrollView.isTracking == NO). - **Manually Triggered onMomentumScrollEnd:** If the scroll view was in motion and is being removed from the window, we manually trigger the `onMomentumScrollEnd` event to ensure that the final scroll state is captured. **_I had fixed this issue on both Old and New arch._** Pull Request resolved: https://github.com/facebook/react-native/pull/46277 Test Plan: Attaching a video with working solution: https://github.com/user-attachments/assets/1a1f3765-3f11-46c3-af18-330c88478db8 Reviewed By: andrewdacenko Differential Revision: D62374798 Pulled By: cipolleschi fbshipit-source-id: 014be8d313bab0257459dc4e53f5b0386a39d5e0 --- .../ScrollView/RCTScrollViewComponentView.mm | 23 +++++++++++++++++++ .../React/Views/ScrollView/RCTScrollView.m | 13 +++++++++++ 2 files changed, 36 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 9447552490a..1e72d941bc4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -721,6 +721,29 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu [self _handleFinishedScrolling:scrollView]; } +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + + if (!self.window) { + // The view is being removed, ensure that the scroll end event is dispatched + [self _handleScrollEndIfNeeded]; + } +} + +- (void)_handleScrollEndIfNeeded +{ + if (_scrollView.isDecelerating || !_scrollView.isTracking) { + if (!_eventEmitter) { + return; + } + static_cast(*_eventEmitter).onMomentumScrollEnd([self _scrollViewMetrics]); + + [self _updateStateWithContentOffset]; + _isUserTriggeredScrolling = NO; + } +} + - (void)_handleFinishedScrolling:(UIScrollView *)scrollView { [self _forceDispatchNextScrollEvent]; diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollView.m b/packages/react-native/React/Views/ScrollView/RCTScrollView.m index 81bdc49c3ed..843bbd7cf85 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.m @@ -848,6 +848,19 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop) RCT_FORWARD_SCROLL_EVENT(scrollViewDidEndZooming : scrollView withView : view atScale : scale); } +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + if (self.window == nil) { + // Check if the ScrollView was in motion + if (_scrollView.isDecelerating || !_scrollView.isTracking) { + // Trigger the onMomentumScrollEnd event manually + RCT_SEND_SCROLL_EVENT(onMomentumScrollEnd, nil); + RCT_FORWARD_SCROLL_EVENT(scrollViewDidEndDecelerating : _scrollView); + } + } +} + - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // Fire a final scroll event