Skip to content

Commit fe7e402

Browse files
committed
Attach native gesture directly to button on the native side on iOS
1 parent c205be2 commit fe7e402

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

packages/react-native-gesture-handler/apple/RNGestureHandlerActionType.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#import <Foundation/Foundation.h>
22

33
typedef NS_ENUM(NSInteger, RNGestureHandlerActionType) {
4+
RNGestureHandlerActionTypeNone = 0, // Handler managed by a native component, doesn't dispatch events to JS
45
RNGestureHandlerActionTypeReanimatedWorklet = 1, // Reanimated worklet
56
RNGestureHandlerActionTypeNativeAnimatedEvent, // Animated.event with useNativeDriver: true
67
RNGestureHandlerActionTypeJSFunctionOldAPI, // JS function or Animated.event with useNativeDriver: false using old

packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <react/renderer/components/view/ViewProps.h>
1212

1313
#import "RNGestureHandlerButton.h"
14+
#import "RNGestureHandlerModule.h"
1415

1516
using namespace facebook::react;
1617

@@ -36,6 +37,7 @@ @implementation RNGestureHandlerButtonComponentView {
3637
RNGestureHandlerButton *_buttonView;
3738
BOOL _needsAnimationStateReset;
3839
int _moduleId;
40+
NSNumber *_managedHandlerTag;
3941
}
4042

4143
#if TARGET_OS_OSX
@@ -122,6 +124,8 @@ - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
122124

123125
- (void)prepareForRecycle
124126
{
127+
[self dropManagedHandler];
128+
125129
[self.layer removeAnimationForKey:@"transform"];
126130
self.layer.transform = CATransform3DIdentity;
127131

@@ -254,6 +258,105 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
254258
left:borderMetrics.borderWidths.left];
255259
}
256260

261+
#pragma mark - Managed gesture handler
262+
263+
- (NSDictionary *)buildManagedHandlerConfig:(const RNGestureHandlerButtonProps &)props
264+
{
265+
NSMutableDictionary *config = [NSMutableDictionary new];
266+
config[@"shouldActivateOnStart"] = @NO;
267+
config[@"disallowInterruption"] = @YES;
268+
config[@"yieldsToContinuousGestures"] = @YES;
269+
config[@"enabled"] = @(props.enabled);
270+
config[@"shouldCancelWhenOutside"] = @(props.cancelOnLeave);
271+
272+
if (!props.gestureTestID.empty()) {
273+
config[@"testID"] = RCTNSStringFromString(props.gestureTestID);
274+
}
275+
276+
if (props.gestureHitSlop.top != 0 || props.gestureHitSlop.left != 0 || props.gestureHitSlop.bottom != 0 ||
277+
props.gestureHitSlop.right != 0) {
278+
config[@"hitSlop"] = @{
279+
@"top" : @(props.gestureHitSlop.top),
280+
@"left" : @(props.gestureHitSlop.left),
281+
@"bottom" : @(props.gestureHitSlop.bottom),
282+
@"right" : @(props.gestureHitSlop.right),
283+
};
284+
}
285+
286+
return config;
287+
}
288+
289+
- (void)updateManagedHandler:(const RNGestureHandlerButtonProps &)newProps
290+
oldProps:(const RNGestureHandlerButtonProps *)oldProps
291+
{
292+
if (newProps.handlerTag <= 0) {
293+
[self dropManagedHandler];
294+
return;
295+
}
296+
297+
RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
298+
if (manager == nil) {
299+
return;
300+
}
301+
302+
BOOL tagChanged = _managedHandlerTag == nil || [_managedHandlerTag doubleValue] != newProps.handlerTag;
303+
304+
if (tagChanged) {
305+
[self dropManagedHandler];
306+
307+
NSNumber *handlerTag = @(newProps.handlerTag);
308+
[manager createGestureHandler:@"NativeViewGestureHandler"
309+
tag:handlerTag
310+
config:[self buildManagedHandlerConfig:newProps]];
311+
312+
// Events dispatched by the handler carry the view's reactTag; without it the
313+
// dispatch is skipped (see `handleGesture:fromReset:fromManualStateChange:`).
314+
_buttonView.reactTag = @(self.tag);
315+
// The cast is needed on macOS, where the button is an NSControl and outside
316+
// of the RCTUIView hierarchy.
317+
[manager attachHandlerForDetectorWithTag:handlerTag
318+
toView:(RNGHUIView *)_buttonView
319+
withActionType:RNGestureHandlerActionTypeNone
320+
withHostDetector:nil];
321+
322+
_managedHandlerTag = handlerTag;
323+
_buttonView.managedHandlerTag = handlerTag;
324+
return;
325+
}
326+
327+
BOOL configChanged = oldProps == nullptr || oldProps->enabled != newProps.enabled ||
328+
oldProps->cancelOnLeave != newProps.cancelOnLeave || oldProps->gestureTestID != newProps.gestureTestID ||
329+
oldProps->gestureHitSlop.top != newProps.gestureHitSlop.top ||
330+
oldProps->gestureHitSlop.left != newProps.gestureHitSlop.left ||
331+
oldProps->gestureHitSlop.bottom != newProps.gestureHitSlop.bottom ||
332+
oldProps->gestureHitSlop.right != newProps.gestureHitSlop.right;
333+
334+
if (configChanged) {
335+
// `setConfig:` resets to defaults before applying, so keys omitted from the
336+
// dictionary (e.g. `hitSlop`, `testID`) get cleared rather than kept.
337+
[manager setGestureHandlerConfig:_managedHandlerTag config:[self buildManagedHandlerConfig:newProps]];
338+
}
339+
}
340+
341+
- (void)dropManagedHandler
342+
{
343+
if (_managedHandlerTag == nil) {
344+
return;
345+
}
346+
347+
RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
348+
[manager dropGestureHandler:_managedHandlerTag];
349+
350+
_managedHandlerTag = nil;
351+
_buttonView.managedHandlerTag = nil;
352+
}
353+
354+
- (void)dealloc
355+
{
356+
// On macOS the buttons are not recycled, `prepareForRecycle` may never run.
357+
[self dropManagedHandler];
358+
}
359+
257360
#pragma mark - RNGHButtonPressEventDelegate
258361

259362
- (void)dispatchButtonEvent:(RNGHButtonEventType)type withExtraData:(RNGestureHandlerEventExtraData *)extraData
@@ -475,6 +578,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
475578
if (shouldApplyStartAnimationState) {
476579
[_buttonView applyStartAnimationState];
477580
}
581+
582+
const auto *oldButtonPropsPtr =
583+
treatAsFirstMount ? nullptr : std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps).get();
584+
[self updateManagedHandler:newProps oldProps:oldButtonPropsPtr];
478585
}
479586

480587
#if !TARGET_OS_OSX

packages/react-native-gesture-handler/apple/RNGestureHandlerManager.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,10 @@ - (void)sendEvent:(RNGestureHandlerStateChange *)event
443443
case RNGestureHandlerActionTypeJSFunctionNewAPI:
444444
[self sendEventForJSFunctionNewAPI:event];
445445
break;
446+
447+
case RNGestureHandlerActionTypeNone:
448+
// Consumed on the native side (e.g. by the button), no events are sent to JS.
449+
break;
446450
}
447451
}
448452

0 commit comments

Comments
 (0)