fix no haptics on refresh
This commit is contained in:
@@ -197,3 +197,106 @@ index ac553045a9c0ce77e288277912538d9e131ebc01..d99c8f4db5a07f1e4ffe7e03ff23adce
|
|||||||
|
|
||||||
NSRange visibleGlyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
NSRange visibleGlyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
||||||
|
|
||||||
|
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
|
||||||
|
index 60160efb163d91813fa2ca7ca758b51afcf261e1..4791a4f123f74d9737e2d0cb1b7fa6e129315562 100644
|
||||||
|
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
|
||||||
|
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
|
||||||
|
@@ -39,8 +39,45 @@ - (void)setRefreshing:(BOOL)refreshing
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
+// Fix for https://github.com/facebook/react-native/issues/43388 — on iOS 17.4+
|
||||||
|
+// the pull-to-refresh trigger haptic is permanently suppressed if tintColor is
|
||||||
|
+// set before the control is actually inside the scroll view's view hierarchy.
|
||||||
|
+// Assigning scrollView.refreshControl does NOT insert it immediately (UIKit
|
||||||
|
+// does that lazily on a later layout pass), so attach-time is still too early.
|
||||||
|
+// Instead, store the wanted color and apply it from didMoveToSuperview, and
|
||||||
|
+// refuse direct tintColor sets until the superview is the scroll view.
|
||||||
|
+// Mirrors Bluesky's Paper patch (bluesky-social/social-app#5605).
|
||||||
|
+@interface RCTHapticCompatibleRefreshControl : UIRefreshControl
|
||||||
|
+@property (nonatomic, strong) UIColor *customTintColor;
|
||||||
|
+@end
|
||||||
|
+
|
||||||
|
+@implementation RCTHapticCompatibleRefreshControl
|
||||||
|
+
|
||||||
|
+- (void)didMoveToSuperview
|
||||||
|
+{
|
||||||
|
+ [super didMoveToSuperview];
|
||||||
|
+ if (self.customTintColor) {
|
||||||
|
+ [self setTintColor:self.customTintColor];
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (void)setCustomTintColor:(UIColor *)customTintColor
|
||||||
|
+{
|
||||||
|
+ _customTintColor = customTintColor;
|
||||||
|
+ [self setTintColor:customTintColor];
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+- (void)setTintColor:(UIColor *)tintColor
|
||||||
|
+{
|
||||||
|
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
|
||||||
|
+ [super setTintColor:tintColor];
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+@end
|
||||||
|
+
|
||||||
|
@implementation RCTPullToRefreshViewComponentView {
|
||||||
|
- UIRefreshControl *_refreshControl;
|
||||||
|
+ RCTHapticCompatibleRefreshControl *_refreshControl;
|
||||||
|
RCTScrollViewComponentView *__weak _scrollViewComponentView;
|
||||||
|
// This variable keeps track of whether the view is recycled or not. Once the view is recycled, the component
|
||||||
|
// creates a new instance of UIRefreshControl, resetting the native props to the default values.
|
||||||
|
@@ -66,7 +103,7 @@ - (instancetype)initWithFrame:(CGRect)frame
|
||||||
|
|
||||||
|
- (void)_initializeUIRefreshControl
|
||||||
|
{
|
||||||
|
- _refreshControl = [UIRefreshControl new];
|
||||||
|
+ _refreshControl = [RCTHapticCompatibleRefreshControl new];
|
||||||
|
[_refreshControl addTarget:self
|
||||||
|
action:@selector(handleUIControlEventValueChanged)
|
||||||
|
forControlEvents:UIControlEventValueChanged];
|
||||||
|
@@ -79,6 +116,17 @@ + (ComponentDescriptorProvider)componentDescriptorProvider
|
||||||
|
return concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
+// Recycled instances get all props force-applied in updateProps, which runs
|
||||||
|
+// before the new UIRefreshControl is inserted into the scroll view hierarchy;
|
||||||
|
+// touching the control that early suppresses the pull-to-refresh haptic on
|
||||||
|
+// iOS 17.4+ (react-native#43388). Opting out of recycling keeps every mount on
|
||||||
|
+// the untouched-before-attach path. Refresh controls are rare and cheap, so
|
||||||
|
+// losing recycling for them is negligible.
|
||||||
|
++ (BOOL)shouldBeRecycled
|
||||||
|
+{
|
||||||
|
+ return NO;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
- (void)prepareForRecycle
|
||||||
|
{
|
||||||
|
[super prepareForRecycle];
|
||||||
|
@@ -93,7 +141,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
|
||||||
|
const auto &newConcreteProps = static_cast<const PullToRefreshViewProps &>(*props);
|
||||||
|
|
||||||
|
if (_recycled || newConcreteProps.tintColor != oldConcreteProps.tintColor) {
|
||||||
|
- _refreshControl.tintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor);
|
||||||
|
+ // Goes through RCTHapticCompatibleRefreshControl, which defers the real
|
||||||
|
+ // tintColor set until the control is inside the scroll view (#43388).
|
||||||
|
+ _refreshControl.customTintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_recycled || newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) {
|
||||||
|
@@ -153,7 +203,12 @@ - (void)_updateTitle
|
||||||
|
const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
|
||||||
|
|
||||||
|
if (concreteProps.title.empty()) {
|
||||||
|
- _refreshControl.attributedTitle = nil;
|
||||||
|
+ // Avoid touching the control when there is nothing to clear — writing
|
||||||
|
+ // attributedTitle (even nil) before the control is in the scroll view
|
||||||
|
+ // hierarchy can suppress the pull-to-refresh haptic (#43388).
|
||||||
|
+ if (_refreshControl.attributedTitle != nil) {
|
||||||
|
+ _refreshControl.attributedTitle = nil;
|
||||||
|
+ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,28 @@ Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh
|
|||||||
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
|
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
|
||||||
in the RN repo: https://github.com/facebook/react-native/issues/43388
|
in the RN repo: https://github.com/facebook/react-native/issues/43388
|
||||||
|
|
||||||
|
## RCTPullToRefreshViewComponentView.mm Patch - Same iOS 17.4+ haptic regression on New Arch
|
||||||
|
|
||||||
|
The Paper fix above does not cover Fabric: `RCTPullToRefreshViewComponentView` owns a plain
|
||||||
|
`UIRefreshControl` and applies `tintColor` in `updateProps`, which runs during the Create mount
|
||||||
|
mutation - before `_attach` puts the control on the scroll view - so the haptic regression
|
||||||
|
(react-native#43388) resurfaced on the New Architecture.
|
||||||
|
|
||||||
|
Port of the Paper approach to Fabric (developed and A/B tested in the `fresh-expo57-refresh`
|
||||||
|
repro app against bare RN 0.86.2 and Expo 54/RN 0.81.5 controls):
|
||||||
|
|
||||||
|
- `RCTHapticCompatibleRefreshControl` subclass stores the wanted color in `customTintColor` and
|
||||||
|
refuses direct `setTintColor:` until the superview is the scroll view; `didMoveToSuperview`
|
||||||
|
applies the stored color once actually inside the hierarchy.
|
||||||
|
- `shouldBeRecycled = NO`: recycled instances get all props force-applied in `updateProps` before
|
||||||
|
the new control is inserted into the scroll view, which would re-trigger the bug; opting out of
|
||||||
|
recycling keeps every mount on the untouched-before-attach path.
|
||||||
|
- `_updateTitle` no longer writes `attributedTitle = nil` when there is nothing to clear - even a
|
||||||
|
nil write before attach suppresses the haptic.
|
||||||
|
|
||||||
|
Upstream issue still open as of Aug 2026. Haptics cannot be verified on the simulator - physical
|
||||||
|
device only.
|
||||||
|
|
||||||
## RCTEnhancedScrollView.mm / RCTScrollViewComponentView.mm Patch - centerContent insets stale after content resize on New Arch
|
## RCTEnhancedScrollView.mm / RCTScrollViewComponentView.mm Patch - centerContent insets stale after content resize on New Arch
|
||||||
|
|
||||||
**TODO: Remove after bumping React Native to 0.87+** (fixed upstream by facebook/react-native#56832,
|
**TODO: Remove after bumping React Native to 0.87+** (fixed upstream by facebook/react-native#56832,
|
||||||
|
|||||||
Reference in New Issue
Block a user