Skip to content

Commit

Permalink
iOS: Fix refreshControl layouting (facebook#28236)
Browse files Browse the repository at this point in the history
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: facebook#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
  • Loading branch information
yogevbd authored and facebook-github-bot committed Aug 5, 2020
1 parent b179c4b commit 1b0fb9b
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 27 deletions.
1 change: 1 addition & 0 deletions React/Views/RefreshControl/RCTRefreshControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;

@end
52 changes: 27 additions & 25 deletions React/Views/RefreshControl/RCTRefreshControl.m
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,15 @@ - (void)setCustomRefreshControl:(UIView<RCTCustomRefreshContolProtocol> *)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
Expand Down Expand Up @@ -421,7 +429,7 @@ - (void)layoutSubviews
#if !TARGET_OS_TV
// Adjust the refresh control frame if the scrollview layout changes.
UIView<RCTCustomRefreshContolProtocol> *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}};
}
Expand Down
3 changes: 3 additions & 0 deletions React/Views/ScrollView/RCTScrollableProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, readonly, getter=isRefreshing) BOOL refreshing;

@optional
@property (nonatomic, weak) UIScrollView *scrollView;

@end

0 comments on commit 1b0fb9b

Please sign in to comment.