Skip to content

Commit ce6db39

Browse files
authored
[iOS] Fix button events with numberOfPointers: 0 (#4098)
## Description After #4038, the button started emitting duplicated events with `numberOfPointers: 0` when moving a pointer. This PR addresses that issue. ## Test plan Add `onUpdate={console.log}` to the `Touchable` implementation, drag over any `Touchable` component, and inspect the logs. Before: ``` LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":0,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":0,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":0,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} ``` After: ``` LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} LOG onUpdate {"handlerTag":50,"pointerType":0,"numberOfPointers":1,"pointerInside":1} ```
1 parent 802a15b commit ce6db39

1 file changed

Lines changed: 58 additions & 9 deletions

File tree

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

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
*/
4242
@implementation RNGestureHandlerButton {
4343
BOOL _isTouchInsideBounds;
44+
BOOL _suppressSuperControlActionDispatch;
4445
CALayer *_underlayLayer;
4546
CGFloat _underlayCornerRadii[8]; // [tlH, tlV, trH, trV, blH, blV, brH, brV] outer radii in points
4647
UIEdgeInsets _underlayBorderInsets; // border widths for padding-box inset
@@ -591,53 +592,101 @@ - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
591592
return [super beginTrackingWithTouch:touch withEvent:event];
592593
}
593594

595+
// Mirrors `sendActionsForControlEvents:` but preserves the real `UIEvent`
596+
// so target-actions with a `forEvent:` parameter receive the touches.
597+
// The public `sendActionsForControlEvents:` passes a nil event, which would
598+
// make handlers reading `event.allTouches.count` observe 0 pointers.
599+
- (void)rngh_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event
600+
{
601+
for (id target in [self allTargets]) {
602+
for (NSString *actionName in [self actionsForTarget:target forControlEvent:controlEvents]) {
603+
[self sendAction:NSSelectorFromString(actionName) to:target forEvent:event];
604+
}
605+
}
606+
}
607+
608+
// UIControl's default `touchesMoved:` / `touchesEnded:` invoke
609+
// `{continue|end}TrackingWithTouch:` and THEN dispatch their own Drag* / Up*
610+
// actions via `sendAction:to:forEvent:` using Apple's 70-point retention-offset
611+
// hit-test. That double-fires our handlers (once from our manual dispatch in
612+
// the tracking hooks, once from UIControl's retention-offset path). We swallow
613+
// UIControl's dispatch via this override; the flag is armed by our tracking
614+
// hooks only after our own dispatch has already run.
615+
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
616+
{
617+
if (_suppressSuperControlActionDispatch) {
618+
return;
619+
}
620+
[super sendAction:action to:target forEvent:event];
621+
}
622+
623+
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
624+
{
625+
[super touchesMoved:touches withEvent:event];
626+
_suppressSuperControlActionDispatch = NO;
627+
}
628+
629+
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
630+
{
631+
[super touchesEnded:touches withEvent:event];
632+
_suppressSuperControlActionDispatch = NO;
633+
}
634+
594635
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
595636
{
596-
// DO NOT call super. We are entirely taking over the drag event generation.
637+
// We take over drag event generation to enforce strict hitslop bounds
638+
// (bypassing Apple's retention offset). After our dispatch, set the
639+
// suppress flag so UIControl's default post-tracking dispatch in
640+
// `touchesMoved:` gets swallowed by our `sendAction:to:forEvent:` override.
597641

598642
CGPoint location = [touch locationInView:self];
599643
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
600644
BOOL currentlyInside = CGRectContainsPoint(hitFrame, location);
601645

602646
if (currentlyInside) {
603647
if (!_isTouchInsideBounds) {
604-
[self sendActionsForControlEvents:UIControlEventTouchDragEnter];
648+
[self rngh_sendActionsForControlEvents:UIControlEventTouchDragEnter withEvent:event];
605649
_isTouchInsideBounds = YES;
606650
}
607651

608652
// Targets may call `cancelTrackingWithEvent:` in response to DragEnter.
609653
if (self.tracking) {
610-
[self sendActionsForControlEvents:UIControlEventTouchDragInside];
654+
[self rngh_sendActionsForControlEvents:UIControlEventTouchDragInside withEvent:event];
611655
}
612656
} else {
613657
if (_isTouchInsideBounds) {
614-
[self sendActionsForControlEvents:UIControlEventTouchDragExit];
658+
[self rngh_sendActionsForControlEvents:UIControlEventTouchDragExit withEvent:event];
615659
_isTouchInsideBounds = NO;
616660
}
617661

618662
// Targets may call `cancelTrackingWithEvent:` in response to DragExit.
619663
if (self.tracking) {
620-
[self sendActionsForControlEvents:UIControlEventTouchDragOutside];
664+
[self rngh_sendActionsForControlEvents:UIControlEventTouchDragOutside withEvent:event];
621665
}
622666
}
623667

668+
_suppressSuperControlActionDispatch = YES;
669+
624670
// If `cancelTrackingWithEvent` was called, `self.tracking` will be NO.
625671
return self.tracking;
626672
}
627673

628674
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
629675
{
630-
// Also bypass super here so that the final "up" event respects the
631-
// strict bounds, rather than Apple's 70-point.
676+
// Same rationale as `continueTrackingWithTouch:` — we dispatch the final
677+
// Up* event ourselves using strict hitslop bounds, then set the suppress
678+
// flag so UIControl's default dispatch in `touchesEnded:` gets swallowed.
632679

633680
if (touch != nil) {
634681
CGPoint location = [touch locationInView:self];
635682
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
636683
if (CGRectContainsPoint(hitFrame, location)) {
637-
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
684+
[self rngh_sendActionsForControlEvents:UIControlEventTouchUpInside withEvent:event];
638685
} else {
639-
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
686+
[self rngh_sendActionsForControlEvents:UIControlEventTouchUpOutside withEvent:event];
640687
}
688+
689+
_suppressSuperControlActionDispatch = YES;
641690
}
642691

643692
_isTouchInsideBounds = NO;

0 commit comments

Comments
 (0)