diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm index c593d9ee2155a826352ebca34845aa5792b2eec3..3c26cd737f21116ff0aa48190e97e6c0649b5fac 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm @@ -101,6 +101,20 @@ - (void)setContentOffset:(CGPoint)contentOffset RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y")); } +- (void)setCenterContent:(BOOL)centerContent +{ + if (_centerContent != centerContent) { + _centerContent = centerContent; + [self centerContentIfNeeded]; + } +} + +- (void)setContentSize:(CGSize)contentSize +{ + [super setContentSize:contentSize]; + [self centerContentIfNeeded]; +} + - (void)setFrame:(CGRect)frame { [super setFrame:frame]; diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h index 914a2494a57923fbf185644b7e2bb8aca8848e56..0deac55f22350f5e8377d8963fb1c2434bf6abfd 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h @@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN */ @interface RCTPullToRefreshViewComponentView : RCTViewComponentView +- (void)beginRefreshingProgrammatically; + @end NS_ASSUME_NONNULL_END diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm index 0d231bc8aa938da296eb3b981e8ac9595a43b87f..be0a10d9c4de1892fa00bcbf8d63d739b66d8ffe 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm @@ -76,7 +76,17 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & return; } - const auto &oldConcreteProps = static_cast(*_props); + /* + * TODO: Remove after upgrading React Native to 0.82+ (fixed upstream by + * facebook/react-native#52615, #52584 and #53231). + * Diff against oldProps instead of _props. During the initial-layout replay + * from layoutSubviews, _props already holds the new props, so diffing + * against it is a no-op and tintColor/progressViewOffset are never applied + * on mount (facebook/react-native#56343). oldProps is null-guarded because + * the create-mutation path passes nullptr. + */ + const auto &oldConcreteProps = static_cast( + oldProps ? *oldProps : *PullToRefreshViewShadowNode::defaultSharedProps()); const auto &newConcreteProps = static_cast(*props); if (newConcreteProps.tintColor != oldConcreteProps.tintColor) { diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 1494fd225aff1fa0429e917404d6b4ca5fc961c5..d0cce700090245444f8ce51e517d5ceca09526f6 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -380,7 +380,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & MAP_SCROLL_VIEW_PROP(zoomScale); - if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) { + // When disabling centerContent, reset inset to prop value + // (enabling is handled automatically by the setCenterContent: setter) + if (oldScrollViewProps.centerContent && !newScrollViewProps.centerContent) { + _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset); + } + + // Only apply contentInset from props if centerContent is disabled + // When centerContent is enabled, the inset is calculated by centerContentIfNeeded + if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset && !newScrollViewProps.centerContent) { _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset); } @@ -507,7 +515,7 @@ - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event } } - return isPointInside ? self : nil; + return isPointInside ? _scrollView : nil; } /* @@ -1038,6 +1046,11 @@ - (void)_adjustForMaintainVisibleContentPosition } } ++ (BOOL)shouldBeRecycled +{ + return NO; +} + @end Class RCTScrollViewCls(void) diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h index e9b330fa7c29c42653a3b0191d0f8a1b13b2d3de..ec5f58c887bfd949f1279ef1c31352e0b465e9ec 100644 --- a/React/Views/RefreshControl/RCTRefreshControl.h +++ b/React/Views/RefreshControl/RCTRefreshControl.h @@ -15,5 +15,8 @@ @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, copy) UIColor *customTintColor; + +- (void)forwarderBeginRefreshing; @end diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m index 53bfd04703502d5b8e932c47a528bb03cd79d330..ff1b1ed5e060bcf0d91528c6d3c2c5c8acf24967 100644 --- a/React/Views/RefreshControl/RCTRefreshControl.m +++ b/React/Views/RefreshControl/RCTRefreshControl.m @@ -23,6 +23,7 @@ @implementation RCTRefreshControl { UIColor *_titleColor; CGFloat _progressViewOffset; BOOL _hasMovedToWindow; + UIColor *_customTintColor; } - (instancetype)init @@ -58,6 +59,12 @@ - (void)layoutSubviews _isInitialRender = false; } +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + [self setTintColor:_customTintColor]; +} + - (void)didMoveToWindow { [super didMoveToWindow]; @@ -221,4 +228,50 @@ - (void)refreshControlValueChanged } } +// Fix for https://github.com/facebook/react-native/issues/43388 +// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor +// is set before the refresh control gets added to the scrollview. We'll call this +// function whenever the superview changes. We'll also call it if the value of customTintColor +// changes. +- (void)setTintColor:(UIColor *)tintColor +{ + if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) { + [super setTintColor:tintColor]; + } +} + +// This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native +// libraries to perform a refresh of a scrollview and access the refresh control's onRefresh +// function. +- (void)forwarderBeginRefreshing +{ + _refreshingProgrammatically = NO; + + [self sizeToFit]; + + if (!self.scrollView) { + return; + } + + UIScrollView *scrollView = (UIScrollView *)self.scrollView; + + [UIView animateWithDuration:0.3 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^(void) { + // Whenever we call this method, the scrollview will always be at a position of + // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl + [scrollView setContentOffset:CGPointMake(0, -65)]; + } + completion:^(__unused BOOL finished) { + [super beginRefreshing]; + [self setCurrentRefreshingState:super.refreshing]; + + if (self->_onRefresh) { + self->_onRefresh(nil); + } + } + ]; +} + @end diff --git a/React/Views/RefreshControl/RCTRefreshControlManager.m b/React/Views/RefreshControl/RCTRefreshControlManager.m index 40aaf9c51ebda9fedb1d1db2e9aacec84b4c39c8..1c60164b69762997b3369b46609a07768a06bad3 100644 --- a/React/Views/RefreshControl/RCTRefreshControlManager.m +++ b/React/Views/RefreshControl/RCTRefreshControlManager.m @@ -22,11 +22,12 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) -RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat) +RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor) + RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt index 8b6571698fc5dd091a0d8980a33bb40295faf305..27c97bfeb6f13907c89f1d85f2bb8b8af7bdfb43 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt @@ -313,8 +313,9 @@ public open class JavaTimerManager( // We also capture the idleCallbackRunnable to tentatively fix: // https://github.com/facebook/react-native/issues/44842 currentIdleCallbackRunnable?.cancel() - currentIdleCallbackRunnable = IdleCallbackRunnable(frameTimeNanos) - reactApplicationContext.runOnJSQueueThread(currentIdleCallbackRunnable) + val idleCallbackRunnable = IdleCallbackRunnable(frameTimeNanos) + currentIdleCallbackRunnable = idleCallbackRunnable + reactApplicationContext.runOnJSQueueThread(idleCallbackRunnable) reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.IDLE_EVENT, this) } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 89b666dcf0258df0702c812600b685463128294c..2b1c3971f0c31a0d7a592b90170e4cc53a8a69dd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -431,6 +431,13 @@ public open class ReactViewGroup public constructor(context: Context?) : inSubviewClippingLoop = true var clippedSoFar = 0 for (i in 0..