mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
0062b10b56
Summary: UIRefreshControl has a tight integration with iOS in terms of UINavigationBar/UIScrollView. In this regard, whenever there's a UIScrollView with a UINavigationBar on top, the OS automatically adjusts the contentInset of the UIScrollView to reflect that (something that is exposed to React Native). In a similar manner, UIScrollView passes this along to the attached UIRefreshControl. By setting the frame manually, the RCTRefreshControl component was preventing this behavior. Although having the option is desired, it should not be done by default. In the past it was possible to adjust for this by manually setting the correct value, calculating the statusBar's height/safeAreaInsets.top and appending 44pt (the UINavigationBar height). However, due to changes related to the Dynamic Island (see [here](https://useyourloaf.com/blog/iphone-14-screen-sizes)), the safe area and the status bar size no longer align, making this calculation more tricky. In summary: this changes allows `progressViewOffset` to exist (in order to maintain feature parity with Android) but provides the opportunity for the OSs default behavior to kick in when applicable. | Applying by default | Not applying by default (this change) | :-------------------------:|:-------------------------:  |  ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. For an example, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [iOS] [Fixed] - Fix application of _progressViewOffset in RCTRefreshControl to not occur by default (when value is unset) Pull Request resolved: https://github.com/facebook/react-native/pull/35281 Test Plan: The GIFs attached display the behavior as expected/unexpected. I'm unaware of any tests written for RCTRefreshControl that could be improved to cover this change. Notes appreciated. Reviewed By: sammy-SC Differential Revision: D41302080 Pulled By: cipolleschi fbshipit-source-id: a2a8e6ef1dcc2e73220c2a182b4516f3bbd94f60
207 lines
5.9 KiB
Objective-C
207 lines
5.9 KiB
Objective-C
/*
|
|
* Copyright (c) Meta Platforms, Inc. and 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;
|
|
CGFloat _progressViewOffset;
|
|
}
|
|
|
|
- (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];
|
|
[self _applyProgressViewOffset];
|
|
|
|
// Fix for bug #7976
|
|
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) {
|
|
[self beginRefreshingProgrammatically];
|
|
}
|
|
_isInitialRender = false;
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
}
|
|
|
|
- (void)_applyProgressViewOffset
|
|
{
|
|
// Setting the UIRefreshControl's frame breaks integration with ContentInset from the superview
|
|
// if it is a UIScrollView. This integration happens when setting the UIScrollView's .refreshControl
|
|
// property. For this reason, setting the frame manually should be avoided, if not needed.
|
|
if (_progressViewOffset == 0.f) {
|
|
return;
|
|
}
|
|
|
|
// progressViewOffset must be converted from the ScrollView parent's coordinate space to
|
|
// the coordinate space of the RefreshControl. This ensures that the control respects any
|
|
// offset in the view hierarchy, and that progressViewOffset is not inadvertently applied
|
|
// multiple times.
|
|
UIView *scrollView = self.superview;
|
|
UIView *target = scrollView.superview;
|
|
CGPoint rawOffset = CGPointMake(0, _progressViewOffset);
|
|
CGPoint converted = [self convertPoint:rawOffset fromView:target];
|
|
self.frame = CGRectOffset(self.frame, 0, converted.y);
|
|
}
|
|
|
|
- (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)setProgressViewOffset:(CGFloat)offset
|
|
{
|
|
_progressViewOffset = offset;
|
|
[self _applyProgressViewOffset];
|
|
}
|
|
|
|
- (void)refreshControlValueChanged
|
|
{
|
|
[self setCurrentRefreshingState:super.refreshing];
|
|
_refreshingProgrammatically = NO;
|
|
|
|
if (_onRefresh) {
|
|
_onRefresh(nil);
|
|
}
|
|
}
|
|
|
|
@end
|