mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
e67811e7a6
Summary: Since iOS 14 refresh control is sometimes visible when it shouldn't. It seems to happen when it is removed and added back to the window. This repros easily when using react-native-screens with react-navigation tabs. Inactive tabs are detached from the window to save resources. Calling endRefreshing when refresh control is added to the window fixes the layout. It will also be called on first mount where it is not necessary, but should be a no-op and didn't cause any issues. I also decided to call it for all ios versions, although it is only needed on iOS 14+ to avoid forking behavior more. ## Changelog [iOS] [Fixed] - Fix RefreshControl layout when removed from window Pull Request resolved: https://github.com/facebook/react-native/pull/31024 Test Plan: Before: https://user-images.githubusercontent.com/2677334/108666197-93ea5a80-74a4-11eb-839b-8a4916967bf8.mov After: https://user-images.githubusercontent.com/2677334/108666223-9ea4ef80-74a4-11eb-8489-4e5d257299c8.mov Reviewed By: shergin Differential Revision: D26590759 Pulled By: PeteTheHeat fbshipit-source-id: b8c06068a24446b261cbeb88ff166289724031f1
186 lines
5.0 KiB
Objective-C
186 lines
5.0 KiB
Objective-C
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTRefreshControl.h"
|
|
#import "RCTRefreshableProtocol.h"
|
|
|
|
#import "RCTUtils.h"
|
|
|
|
@interface RCTRefreshControl () <RCTRefreshableProtocol>
|
|
@end
|
|
|
|
@implementation RCTRefreshControl {
|
|
BOOL _isInitialRender;
|
|
BOOL _currentRefreshingState;
|
|
UInt64 _currentRefreshingStateClock;
|
|
UInt64 _currentRefreshingStateTimestamp;
|
|
BOOL _refreshingProgrammatically;
|
|
NSString *_title;
|
|
UIColor *_titleColor;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
[self addTarget:self action:@selector(refreshControlValueChanged) forControlEvents:UIControlEventValueChanged];
|
|
_currentRefreshingStateClock = 1;
|
|
_currentRefreshingStateTimestamp = 0;
|
|
_isInitialRender = true;
|
|
_currentRefreshingState = false;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
[super layoutSubviews];
|
|
|
|
// If the control is refreshing when mounted we need to call
|
|
// beginRefreshing in layoutSubview or it doesn't work.
|
|
if (_currentRefreshingState && _isInitialRender) {
|
|
[self beginRefreshingProgrammatically];
|
|
}
|
|
_isInitialRender = false;
|
|
}
|
|
|
|
- (void)didMoveToWindow
|
|
{
|
|
[super didMoveToWindow];
|
|
|
|
// Since iOS 14 there seems to be a bug where refresh control becomes
|
|
// visible if the view gets removed from window then added back again.
|
|
// Calling endRefreshing fixes the layout.
|
|
if (!_currentRefreshingState) {
|
|
[super endRefreshing];
|
|
}
|
|
}
|
|
|
|
- (void)beginRefreshingProgrammatically
|
|
{
|
|
UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp;
|
|
_refreshingProgrammatically = YES;
|
|
|
|
// Fix for bug #24855
|
|
[self sizeToFit];
|
|
|
|
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 = 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
|
|
delay:0
|
|
options:UIViewAnimationOptionBeginFromCurrentState
|
|
animations:^(void) {
|
|
[scrollView setContentOffset:offset];
|
|
}
|
|
completion:^(__unused BOOL finished) {
|
|
if (endRefreshingTimestamp == self->_currentRefreshingStateTimestamp) {
|
|
[super endRefreshing];
|
|
[self setCurrentRefreshingState:super.refreshing];
|
|
}
|
|
}];
|
|
} else {
|
|
[super endRefreshing];
|
|
}
|
|
}
|
|
|
|
- (NSString *)title
|
|
{
|
|
return _title;
|
|
}
|
|
|
|
- (void)setTitle:(NSString *)title
|
|
{
|
|
_title = title;
|
|
[self _updateTitle];
|
|
}
|
|
|
|
- (void)setTitleColor:(UIColor *)color
|
|
{
|
|
_titleColor = color;
|
|
[self _updateTitle];
|
|
}
|
|
|
|
- (void)_updateTitle
|
|
{
|
|
if (!_title) {
|
|
return;
|
|
}
|
|
|
|
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
|
|
if (_titleColor) {
|
|
attributes[NSForegroundColorAttributeName] = _titleColor;
|
|
}
|
|
|
|
self.attributedTitle = [[NSAttributedString alloc] initWithString:_title attributes:attributes];
|
|
}
|
|
|
|
- (void)setRefreshing:(BOOL)refreshing
|
|
{
|
|
if (_currentRefreshingState != refreshing) {
|
|
[self setCurrentRefreshingState:refreshing];
|
|
|
|
if (refreshing) {
|
|
if (!_isInitialRender) {
|
|
[self beginRefreshingProgrammatically];
|
|
}
|
|
} else {
|
|
[self endRefreshingProgrammatically];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setCurrentRefreshingState:(BOOL)refreshing
|
|
{
|
|
_currentRefreshingState = refreshing;
|
|
_currentRefreshingStateTimestamp = _currentRefreshingStateClock++;
|
|
}
|
|
|
|
- (void)refreshControlValueChanged
|
|
{
|
|
[self setCurrentRefreshingState:super.refreshing];
|
|
_refreshingProgrammatically = NO;
|
|
|
|
if (_onRefresh) {
|
|
_onRefresh(nil);
|
|
}
|
|
}
|
|
|
|
@end
|