1111#import < react/renderer/components/view/ViewProps.h>
1212
1313#import " RNGestureHandlerButton.h"
14+ #import " RNGestureHandlerModule.h"
1415
1516using 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
0 commit comments