Skip to content

Commit 768d75d

Browse files
authored
[iOS] Ignore Apple's default retention offset for buttons and rely strictly on hitslop instead (#4038)
## Description Apple enforces a stupidly large retention offset in the default UIControl touch tracking loop, which was conflicting with `shouldCancelWhenOutside` and `hitSlop` props. This PR overrides the default tracking loop to rely only on our props instead. ## Test plan Tested on the example added in #4018
1 parent 23d96d9 commit 768d75d

1 file changed

Lines changed: 60 additions & 0 deletions

File tree

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@
4141
*/
4242
@implementation RNGestureHandlerButton {
4343
CALayer *_underlayLayer;
44+
BOOL _isTouchInsideBounds;
4445
}
4546

4647
- (void)commonInit
4748
{
49+
_isTouchInsideBounds = NO;
4850
_hitTestEdgeInsets = UIEdgeInsetsZero;
4951
_userEnabled = YES;
5052
_pointerEvents = RNGestureHandlerPointerEventsAuto;
@@ -275,6 +277,64 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
275277
return CGRectContainsPoint(hitFrame, point);
276278
}
277279

280+
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
281+
{
282+
_isTouchInsideBounds = YES;
283+
return [super beginTrackingWithTouch:touch withEvent:event];
284+
}
285+
286+
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
287+
{
288+
// DO NOT call super. We are entirely taking over the drag event generation.
289+
290+
CGPoint location = [touch locationInView:self];
291+
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
292+
BOOL currentlyInside = CGRectContainsPoint(hitFrame, location);
293+
294+
if (currentlyInside) {
295+
if (!_isTouchInsideBounds) {
296+
[self sendActionsForControlEvents:UIControlEventTouchDragEnter];
297+
_isTouchInsideBounds = YES;
298+
}
299+
300+
// Targets may call `cancelTrackingWithEvent:` in response to DragEnter.
301+
if (self.tracking) {
302+
[self sendActionsForControlEvents:UIControlEventTouchDragInside];
303+
}
304+
} else {
305+
if (_isTouchInsideBounds) {
306+
[self sendActionsForControlEvents:UIControlEventTouchDragExit];
307+
_isTouchInsideBounds = NO;
308+
}
309+
310+
// Targets may call `cancelTrackingWithEvent:` in response to DragExit.
311+
if (self.tracking) {
312+
[self sendActionsForControlEvents:UIControlEventTouchDragOutside];
313+
}
314+
}
315+
316+
// If `cancelTrackingWithEvent` was called, `self.tracking` will be NO.
317+
return self.tracking;
318+
}
319+
320+
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
321+
{
322+
// Also bypass super here so that the final "up" event respects the
323+
// strict bounds, rather than Apple's 70-point.
324+
325+
if (touch != nil) {
326+
CGPoint location = [touch locationInView:self];
327+
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
328+
if (CGRectContainsPoint(hitFrame, location)) {
329+
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
330+
} else {
331+
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
332+
}
333+
}
334+
335+
_isTouchInsideBounds = NO;
336+
}
337+
278338
- (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
279339
{
280340
RNGestureHandlerPointerEvents pointerEvents = _pointerEvents;

0 commit comments

Comments
 (0)