From 1b0fb9bead4d158d14df5a994423d06716b5e377 Mon Sep 17 00:00:00 2001 From: Yogev Ben David Date: Wed, 5 Aug 2020 11:48:36 -0700 Subject: [PATCH] iOS: Fix refreshControl layouting (#28236) Summary: In `react-native-navigation` we allow the usage of native iOS navigationBar **largeTitle** which cause the title to "jump" when pulling to refresh. We found that the layout calculations of the refreshControl element mess up the system behaviour. ## Changelog [iOS] [Fixed] - Fix refreshControl messes up navigationBar largeTitles Pull Request resolved: https://github.com/facebook/react-native/pull/28236 Test Plan: ### Before the fix: ![before](https://user-images.githubusercontent.com/10794586/75991307-f7c7ec00-5efe-11ea-8cd9-ab8c3fbe1dc1.gif) ### And after: ![after](https://user-images.githubusercontent.com/10794586/75990618-d9152580-5efd-11ea-8c72-5deb6d83a840.gif) ### How it looks like with react-native init app after the fix: ![ezgif com-video-to-gif (4)](https://user-images.githubusercontent.com/10794586/77253369-54970680-6c62-11ea-9ad6-3265e23044e6.gif) Reviewed By: sammy-SC Differential Revision: D22782680 Pulled By: PeteTheHeat fbshipit-source-id: f86ccd0a6ad492312029a69b392cd525450fe594 --- .../Views/RefreshControl/RCTRefreshControl.h | 1 + .../Views/RefreshControl/RCTRefreshControl.m | 52 ++++++++++--------- React/Views/ScrollView/RCTScrollView.m | 12 ++++- .../Views/ScrollView/RCTScrollableProtocol.h | 3 ++ 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h index 817576523835f5..c17b226e069fdc 100644 --- a/React/Views/RefreshControl/RCTRefreshControl.h +++ b/React/Views/RefreshControl/RCTRefreshControl.h @@ -14,5 +14,6 @@ @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) RCTDirectEventBlock onRefresh; +@property (nonatomic, weak) UIScrollView *scrollView; @end diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m index 428a5e7fa406b9..1022b7896d8867 100644 --- a/React/Views/RefreshControl/RCTRefreshControl.m +++ b/React/Views/RefreshControl/RCTRefreshControl.m @@ -41,12 +41,6 @@ - (void)layoutSubviews { [super layoutSubviews]; - // Fix for bug #7976 - // TODO: Remove when updating to use iOS 10 refreshControl UIScrollView prop. - if (self.backgroundColor == nil) { - self.backgroundColor = [UIColor clearColor]; - } - // If the control is refreshing when mounted we need to call // beginRefreshing in layoutSubview or it doesn't work. if (_currentRefreshingState && _isInitialRender) { @@ -59,34 +53,42 @@ - (void)beginRefreshingProgrammatically { UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp; _refreshingProgrammatically = YES; - // When using begin refreshing we need to adjust the ScrollView content offset manually. - UIScrollView *scrollView = (UIScrollView *)self.superview; + // Fix for bug #24855 [self sizeToFit]; - CGPoint offset = {scrollView.contentOffset.x, scrollView.contentOffset.y - self.frame.size.height}; - - // `beginRefreshing` must be called after the animation is done. This is why it is impossible - // to use `setContentOffset` with `animated:YES`. - [UIView animateWithDuration:0.25 - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState - animations:^(void) { - [scrollView setContentOffset:offset]; - } - completion:^(__unused BOOL finished) { - if (beginRefreshingTimestamp == self->_currentRefreshingStateTimestamp) { - [super beginRefreshing]; - [self setCurrentRefreshingState:super.refreshing]; + + if (self.scrollView) { + // When using begin refreshing we need to adjust the ScrollView content offset manually. + UIScrollView *scrollView = (UIScrollView *)self.scrollView; + + CGPoint offset = {scrollView.contentOffset.x, scrollView.contentOffset.y - self.frame.size.height}; + + // `beginRefreshing` must be called after the animation is done. This is why it is impossible + // to use `setContentOffset` with `animated:YES`. + [UIView animateWithDuration:0.25 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^(void) { + [scrollView setContentOffset:offset]; } - }]; + completion:^(__unused BOOL finished) { + if (beginRefreshingTimestamp == self->_currentRefreshingStateTimestamp) { + [super beginRefreshing]; + [self setCurrentRefreshingState:super.refreshing]; + } + }]; + } else if (beginRefreshingTimestamp == self->_currentRefreshingStateTimestamp) { + [super beginRefreshing]; + [self setCurrentRefreshingState:super.refreshing]; + } } - (void)endRefreshingProgrammatically { // The contentOffset of the scrollview MUST be greater than the contentInset before calling // endRefreshing otherwise the next pull to refresh will not work properly. - UIScrollView *scrollView = (UIScrollView *)self.superview; - if (_refreshingProgrammatically && scrollView.contentOffset.y < -scrollView.contentInset.top) { + UIScrollView *scrollView = self.scrollView; + if (scrollView && _refreshingProgrammatically && scrollView.contentOffset.y < -scrollView.contentInset.top) { UInt64 endRefreshingTimestamp = _currentRefreshingStateTimestamp; CGPoint offset = {scrollView.contentOffset.x, -scrollView.contentInset.top}; [UIView animateWithDuration:0.25 diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index acef7bc88538b6..80a7d09714c51c 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -230,7 +230,15 @@ - (void)setCustomRefreshControl:(UIView *)refres [_customRefreshControl removeFromSuperview]; } _customRefreshControl = refreshControl; - [self addSubview:_customRefreshControl]; + // We have to set this because we can't always guarantee the + // `RCTCustomRefreshContolProtocol`'s superview will always be of class + // `UIScrollView` like we were previously + _customRefreshControl.scrollView = self; + if ([refreshControl isKindOfClass:UIRefreshControl.class]) { + self.refreshControl = (UIRefreshControl *)refreshControl; + } else { + [self addSubview:_customRefreshControl]; + } } - (void)setPinchGestureEnabled:(BOOL)pinchGestureEnabled @@ -421,7 +429,7 @@ - (void)layoutSubviews #if !TARGET_OS_TV // Adjust the refresh control frame if the scrollview layout changes. UIView *refreshControl = _scrollView.customRefreshControl; - if (refreshControl && refreshControl.isRefreshing) { + if (refreshControl && refreshControl.isRefreshing && ![refreshControl isKindOfClass:UIRefreshControl.class]) { refreshControl.frame = (CGRect){_scrollView.contentOffset, {_scrollView.frame.size.width, refreshControl.frame.size.height}}; } diff --git a/React/Views/ScrollView/RCTScrollableProtocol.h b/React/Views/ScrollView/RCTScrollableProtocol.h index 7800e796cc8a6e..ec6772e8f31421 100644 --- a/React/Views/ScrollView/RCTScrollableProtocol.h +++ b/React/Views/ScrollView/RCTScrollableProtocol.h @@ -38,4 +38,7 @@ @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, readonly, getter=isRefreshing) BOOL refreshing; +@optional +@property (nonatomic, weak) UIScrollView *scrollView; + @end