Skip to content

Commit 1c45168

Browse files
committed
Attach native gesture directly to button on the native side on iOS
1 parent 2ac7174 commit 1c45168

3 files changed

Lines changed: 110 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: 105 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,103 @@ - (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+
react_native_assert(manager != nil && "Tried to access a non-existent handler manager");
299+
300+
BOOL tagChanged = _managedHandlerTag == nil || [_managedHandlerTag doubleValue] != newProps.handlerTag;
301+
302+
if (tagChanged) {
303+
[self dropManagedHandler];
304+
305+
NSNumber *handlerTag = @(newProps.handlerTag);
306+
[manager createGestureHandler:@"NativeViewGestureHandler"
307+
tag:handlerTag
308+
config:[self buildManagedHandlerConfig:newProps]];
309+
310+
// Events dispatched by the handler carry the view's reactTag; without it the
311+
// dispatch is skipped (see `handleGesture:fromReset:fromManualStateChange:`).
312+
_buttonView.reactTag = @(self.tag);
313+
// The cast is needed on macOS, where the button is an NSControl and outside
314+
// of the RCTUIView hierarchy.
315+
[manager attachHandlerForDetectorWithTag:handlerTag
316+
toView:(RNGHUIView *)_buttonView
317+
withActionType:RNGestureHandlerActionTypeNone
318+
withHostDetector:nil];
319+
320+
_managedHandlerTag = handlerTag;
321+
_buttonView.managedHandlerTag = handlerTag;
322+
return;
323+
}
324+
325+
BOOL configChanged = oldProps == nullptr || oldProps->enabled != newProps.enabled ||
326+
oldProps->cancelOnLeave != newProps.cancelOnLeave || oldProps->gestureTestID != newProps.gestureTestID ||
327+
oldProps->gestureHitSlop.top != newProps.gestureHitSlop.top ||
328+
oldProps->gestureHitSlop.left != newProps.gestureHitSlop.left ||
329+
oldProps->gestureHitSlop.bottom != newProps.gestureHitSlop.bottom ||
330+
oldProps->gestureHitSlop.right != newProps.gestureHitSlop.right;
331+
332+
if (configChanged) {
333+
// `setConfig:` resets to defaults before applying, so keys omitted from the
334+
// dictionary (e.g. `hitSlop`, `testID`) get cleared rather than kept.
335+
[manager setGestureHandlerConfig:_managedHandlerTag config:[self buildManagedHandlerConfig:newProps]];
336+
}
337+
}
338+
339+
- (void)dropManagedHandler
340+
{
341+
if (_managedHandlerTag == nil) {
342+
return;
343+
}
344+
345+
RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
346+
[manager dropGestureHandler:_managedHandlerTag];
347+
348+
_managedHandlerTag = nil;
349+
_buttonView.managedHandlerTag = nil;
350+
}
351+
352+
- (void)dealloc
353+
{
354+
// On macOS the buttons are not recycled, `prepareForRecycle` may never run.
355+
[self dropManagedHandler];
356+
}
357+
257358
#pragma mark - RNGHButtonPressEventDelegate
258359

259360
- (void)dispatchButtonEvent:(RNGHButtonEventType)type withExtraData:(RNGestureHandlerEventExtraData *)extraData
@@ -475,6 +576,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
475576
if (shouldApplyStartAnimationState) {
476577
[_buttonView applyStartAnimationState];
477578
}
579+
580+
const auto *oldButtonPropsPtr =
581+
treatAsFirstMount ? nullptr : std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps).get();
582+
[self updateManagedHandler:newProps oldProps:oldButtonPropsPtr];
478583
}
479584

480585
#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)