From 52aa47e46a1c8df91b8c933766b76a2dda05dbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 23 Apr 2026 16:15:21 +0200 Subject: [PATCH 01/19] Attach to UITextField --- .../apple/Handlers/RNNativeViewHandler.mm | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 5e67f54326..5020f6b3fd 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -14,6 +14,7 @@ #import #import +#import #import #pragma mark RNDummyGestureRecognizer @@ -135,15 +136,28 @@ - (void)updateConfig:(NSDictionary *)config - (void)bindToView:(UIView *)view { + UIControl *control = nil; + // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow // for properties like `disallowInterruption` to work. if ([view isKindOfClass:[UIControl class]]) { - UIControl *control = (UIControl *)view; + control = (UIControl *)view; + } else if ([view isKindOfClass:[RCTTextInputComponentView class]]) { + // TextInput (RCTTextInputComponentView) contains a UITextField or UITextView as a subview. + // We need to attach to that subview to receive touch events. + for (UIView *subview in view.subviews) { + if ([subview isKindOfClass:[UITextField class]] || [subview isKindOfClass:[UITextView class]]) { + control = (UIControl *)subview; + break; + } + } + } + if (control) { // Pressing UISwitch triggers only touchUp and valueChanged callbacks. In order to align its behavior // with other UIControls, we have to dispatch full Gesture Handler events flow in one callback, as // touchesDown is not executed. - if ([view isKindOfClass:[UISwitch class]]) { + if ([control isKindOfClass:[UISwitch class]]) { _pointerType = RNGestureHandlerTouch; [control addTarget:self action:@selector(handleSwitch:) forControlEvents:UIControlEventValueChanged]; } else { @@ -183,6 +197,14 @@ - (void)unbindFromView if ([view isKindOfClass:[UIControl class]]) { [(UIControl *)view removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; + } else if ([view isKindOfClass:[RCTTextInputComponentView class]]) { + // Remove targets from the internal UITextField/UITextView + for (UIView *subview in view.subviews) { + if ([subview isKindOfClass:[UITextField class]] || [subview isKindOfClass:[UITextView class]]) { + [(UIControl *)subview removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; + break; + } + } } // Restore the React Native's overriden behavor for not delaying content touches From b00b7af5bb24b64072501b873e045f7a0a82fbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 23 Apr 2026 16:23:27 +0200 Subject: [PATCH 02/19] Store control --- .../apple/Handlers/RNNativeViewHandler.mm | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 5020f6b3fd..3661e29d6d 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -115,6 +115,7 @@ @implementation RNNativeViewGestureHandler { BOOL _shouldActivateOnStart; BOOL _disallowInterruption; RNGestureHandlerEventExtraData *_lastActiveExtraData; + __weak UIControl *_targetControl; } - (instancetype)initWithTag:(NSNumber *)tag @@ -154,6 +155,8 @@ - (void)bindToView:(UIView *)view } if (control) { + _targetControl = control; + // Pressing UISwitch triggers only touchUp and valueChanged callbacks. In order to align its behavior // with other UIControls, we have to dispatch full Gesture Handler events flow in one callback, as // touchesDown is not executed. @@ -195,16 +198,9 @@ - (void)unbindFromView { UIView *view = self.recognizer.view; - if ([view isKindOfClass:[UIControl class]]) { - [(UIControl *)view removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; - } else if ([view isKindOfClass:[RCTTextInputComponentView class]]) { - // Remove targets from the internal UITextField/UITextView - for (UIView *subview in view.subviews) { - if ([subview isKindOfClass:[UITextField class]] || [subview isKindOfClass:[UITextView class]]) { - [(UIControl *)subview removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; - break; - } - } + if (_targetControl) { + [_targetControl removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; + _targetControl = nil; } // Restore the React Native's overriden behavor for not delaying content touches From 6277d69524031a6f32177a910c91804bd17035f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 23 Apr 2026 16:40:07 +0200 Subject: [PATCH 03/19] Use field --- .../apple/Handlers/RNNativeViewHandler.mm | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 3661e29d6d..7f592bd0da 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -115,7 +115,7 @@ @implementation RNNativeViewGestureHandler { BOOL _shouldActivateOnStart; BOOL _disallowInterruption; RNGestureHandlerEventExtraData *_lastActiveExtraData; - __weak UIControl *_targetControl; + __weak UIControl *_control; } - (instancetype)initWithTag:(NSNumber *)tag @@ -137,51 +137,48 @@ - (void)updateConfig:(NSDictionary *)config - (void)bindToView:(UIView *)view { - UIControl *control = nil; - // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow // for properties like `disallowInterruption` to work. if ([view isKindOfClass:[UIControl class]]) { - control = (UIControl *)view; + _control = (UIControl *)view; } else if ([view isKindOfClass:[RCTTextInputComponentView class]]) { // TextInput (RCTTextInputComponentView) contains a UITextField or UITextView as a subview. - // We need to attach to that subview to receive touch events. for (UIView *subview in view.subviews) { if ([subview isKindOfClass:[UITextField class]] || [subview isKindOfClass:[UITextView class]]) { - control = (UIControl *)subview; + _control = (UIControl *)subview; break; } } } - if (control) { - _targetControl = control; - + if (_control) { // Pressing UISwitch triggers only touchUp and valueChanged callbacks. In order to align its behavior // with other UIControls, we have to dispatch full Gesture Handler events flow in one callback, as // touchesDown is not executed. - if ([control isKindOfClass:[UISwitch class]]) { + if ([_control isKindOfClass:[UISwitch class]]) { _pointerType = RNGestureHandlerTouch; - [control addTarget:self action:@selector(handleSwitch:) forControlEvents:UIControlEventValueChanged]; + [_control addTarget:self action:@selector(handleSwitch:) forControlEvents:UIControlEventValueChanged]; } else { - [control addTarget:self action:@selector(handleTouchDown:forEvent:) forControlEvents:UIControlEventTouchDown]; - [control addTarget:self + [_control addTarget:self action:@selector(handleTouchDown:forEvent:) forControlEvents:UIControlEventTouchDown]; + [_control addTarget:self action:@selector(handleTouchUpOutside:forEvent:) forControlEvents:UIControlEventTouchUpOutside]; - [control addTarget:self + [_control addTarget:self action:@selector(handleTouchUpInside:forEvent:) forControlEvents:UIControlEventTouchUpInside]; - [control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit]; - [control addTarget:self + [_control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit]; + [_control addTarget:self action:@selector(handleDragInside:forEvent:) forControlEvents:UIControlEventTouchDragInside]; - [control addTarget:self + [_control addTarget:self action:@selector(handleDragOutside:forEvent:) forControlEvents:UIControlEventTouchDragOutside]; - [control addTarget:self + [_control addTarget:self action:@selector(handleDragEnter:forEvent:) forControlEvents:UIControlEventTouchDragEnter]; - [control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel]; + [_control addTarget:self + action:@selector(handleTouchCancel:forEvent:) + forControlEvents:UIControlEventTouchCancel]; } } else { [super bindToView:view]; @@ -198,9 +195,9 @@ - (void)unbindFromView { UIView *view = self.recognizer.view; - if (_targetControl) { - [_targetControl removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; - _targetControl = nil; + if (_control) { + [_control removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; + _control = nil; } // Restore the React Native's overriden behavor for not delaying content touches From 5cf0f5c59b983a239a5897d02aae2d4875fb5690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Thu, 23 Apr 2026 17:43:20 +0200 Subject: [PATCH 04/19] Claude fix --- .../apple/Handlers/RNNativeViewHandler.mm | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 7f592bd0da..7fa4012744 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -17,6 +17,13 @@ #import #import +#if !TARGET_OS_OSX +@interface RNNativeViewGestureHandler () +- (void)handleTextViewTouchDown:(UIEvent *)event; +- (void)handleTextViewTouchUp:(UIEvent *)event; +@end +#endif + #pragma mark RNDummyGestureRecognizer @implementation RNDummyGestureRecognizer { @@ -36,6 +43,10 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; + + if ([self.view isKindOfClass:[UITextView class]]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchDown:event]; + } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event @@ -47,6 +58,11 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; + + if ([self.view isKindOfClass:[UITextView class]]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchUp:event]; + } + self.state = UIGestureRecognizerStateFailed; // For now, we are handling only the scroll view case. @@ -137,16 +153,23 @@ - (void)updateConfig:(NSDictionary *)config - (void)bindToView:(UIView *)view { + UIView *textInputChild = nil; + // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow // for properties like `disallowInterruption` to work. if ([view isKindOfClass:[UIControl class]]) { _control = (UIControl *)view; } else if ([view isKindOfClass:[RCTTextInputComponentView class]]) { - // TextInput (RCTTextInputComponentView) contains a UITextField or UITextView as a subview. + // TextInput (RCTTextInputComponentView) contains a UITextField (single-line) or UITextView (multi-line) as a + // subview. UITextField is a UIControl, so we can use UIControl events. UITextView is not a UIControl, so we need to + // attach the gesture recognizer to it directly. for (UIView *subview in view.subviews) { - if ([subview isKindOfClass:[UITextField class]] || [subview isKindOfClass:[UITextView class]]) { + if ([subview isKindOfClass:[UITextField class]]) { _control = (UIControl *)subview; break; + } else if ([subview isKindOfClass:[UITextView class]]) { + textInputChild = subview; + break; } } } @@ -182,6 +205,16 @@ - (void)bindToView:(UIView *)view } } else { [super bindToView:view]; + + // For multiline TextInput (UITextView), we need to move the gesture recognizer from the parent + // to the actual text view so it can receive touch events + if (textInputChild != nil) { + UIView *currentRecognizerView = self.recognizer.view; + if (currentRecognizerView != nil) { + [currentRecognizerView removeGestureRecognizer:self.recognizer]; + } + [textInputChild addGestureRecognizer:self.recognizer]; + } } // We can restore default scrollview behaviour to delay touches to scrollview's children @@ -238,6 +271,28 @@ - (void)handleSwitch:(UIView *)sender [self reset]; } +- (void)handleTextViewTouchDown:(UIEvent *)event +{ + [self reset]; + + RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]; + + [self sendEventsInState:RNGestureHandlerStateBegan forViewWithTag:self.viewTag withExtraData:extraData]; + [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:self.viewTag withExtraData:extraData]; + _lastActiveExtraData = extraData; +} + +- (void)handleTextViewTouchUp:(UIEvent *)event +{ + [self sendEventsInState:RNGestureHandlerStateEnd + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; +} + - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event { [self setCurrentPointerTypeForEvent:event]; From af14375bd82c72450e83f07f77da8d70c6c73e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 11:40:08 +0200 Subject: [PATCH 05/19] Macos build --- .../apple/Handlers/RNNativeViewHandler.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 7fa4012744..f45baf946e 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -131,7 +131,9 @@ @implementation RNNativeViewGestureHandler { BOOL _shouldActivateOnStart; BOOL _disallowInterruption; RNGestureHandlerEventExtraData *_lastActiveExtraData; +#if !TARGET_OS_OSX __weak UIControl *_control; +#endif } - (instancetype)initWithTag:(NSNumber *)tag From 46c7bd3ae8524d91c244e0c33b58ec0ed764cdbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 11:49:13 +0200 Subject: [PATCH 06/19] Extract checks to helper --- .../apple/Handlers/RNNativeViewHandler.mm | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index f45baf946e..1cdf1d52ca 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -39,12 +39,17 @@ - (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler } #if !TARGET_OS_OSX +- (BOOL)isAttachedToTextView +{ + return [self.view isKindOfClass:[UITextView class]]; +} + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; - if ([self.view isKindOfClass:[UITextView class]]) { + if ([self isAttachedToTextView]) { [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchDown:event]; } } @@ -59,7 +64,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; - if ([self.view isKindOfClass:[UITextView class]]) { + if ([self isAttachedToTextView]) { [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchUp:event]; } From 716f87b775a8a09f8a14211e4849b58961ccd87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 12:05:42 +0200 Subject: [PATCH 07/19] simplify binding --- .../apple/Handlers/RNNativeViewHandler.mm | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 1cdf1d52ca..ab59cccc0c 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -211,16 +211,13 @@ - (void)bindToView:(UIView *)view forControlEvents:UIControlEventTouchCancel]; } } else { - [super bindToView:view]; - - // For multiline TextInput (UITextView), we need to move the gesture recognizer from the parent - // to the actual text view so it can receive touch events + // For multiline TextInput (UITextView), bind to the child view so the recognizer receives + // touch events directly, then restore viewTag to the parent's react tag. if (textInputChild != nil) { - UIView *currentRecognizerView = self.recognizer.view; - if (currentRecognizerView != nil) { - [currentRecognizerView removeGestureRecognizer:self.recognizer]; - } - [textInputChild addGestureRecognizer:self.recognizer]; + [super bindToView:textInputChild]; + self.viewTag = view.reactTag; + } else { + [super bindToView:view]; } } From 167e57d92db87ea14842b53ed859ce16c48e4491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= <63123542+m-bert@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:36:25 +0200 Subject: [PATCH 08/19] Update packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../apple/Handlers/RNNativeViewHandler.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index ab59cccc0c..ff1a9428fd 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -47,11 +47,12 @@ - (BOOL)isAttachedToTextView - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; - [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; if ([self isAttachedToTextView]) { [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchDown:event]; } + + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event From d99d12f6e9228ecbebe40365f4376c8d4a102fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 12:56:48 +0200 Subject: [PATCH 09/19] Use correct viewTag --- .../apple/RNGestureHandlerPointerTracker.mm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm index e064d0994c..4ed6f494fb 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm @@ -244,13 +244,11 @@ - (void)sendEvent // it may happen that the gesture recognizer is reset after it's been unbound from the view, // it that recognizer tried to send event, the app would crash because the target of the event // would be nil. - if (_gestureHandler.recognizer.view.reactTag == nil && - _gestureHandler.actionType != RNGestureHandlerActionTypeNativeDetector) { + if (_gestureHandler.viewTag == nil && _gestureHandler.actionType != RNGestureHandlerActionTypeNativeDetector) { return; } - [_gestureHandler sendTouchEventInState:[_gestureHandler state] - forViewWithTag:_gestureHandler.recognizer.view.reactTag]; + [_gestureHandler sendTouchEventInState:[_gestureHandler state] forViewWithTag:_gestureHandler.viewTag]; } @end From 1cf2d8982ef18039bcec2970b3563dd37b99cdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 13:00:15 +0200 Subject: [PATCH 10/19] Touch cancel --- .../apple/Handlers/RNNativeViewHandler.mm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index ff1a9428fd..12b95a62e3 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -21,6 +21,7 @@ @interface RNNativeViewGestureHandler () - (void)handleTextViewTouchDown:(UIEvent *)event; - (void)handleTextViewTouchUp:(UIEvent *)event; +- (void)handleTextViewTouchCancel:(UIEvent *)event; @end #endif @@ -81,6 +82,11 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event]; + + if ([self isAttachedToTextView]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchCancel:event]; + } + self.state = UIGestureRecognizerStateCancelled; [self reset]; } @@ -298,6 +304,15 @@ - (void)handleTextViewTouchUp:(UIEvent *)event withPointerType:_pointerType]]; } +- (void)handleTextViewTouchCancel:(UIEvent *)event +{ + [self sendEventsInState:RNGestureHandlerStateCancelled + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; +} + - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event { [self setCurrentPointerTypeForEvent:event]; From 7cc6973d250df8c830c03df65e8e18232d0366fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 13:03:57 +0200 Subject: [PATCH 11/19] pointerInside --- .../apple/Handlers/RNNativeViewHandler.mm | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 12b95a62e3..75c3f67173 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -297,11 +297,21 @@ - (void)handleTextViewTouchDown:(UIEvent *)event - (void)handleTextViewTouchUp:(UIEvent *)event { - [self sendEventsInState:RNGestureHandlerStateEnd - forViewWithTag:self.viewTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; + BOOL isInside = [self containsPointInView]; + + if (!isInside && self.shouldCancelWhenOutside) { + [self sendEventsInState:RNGestureHandlerStateFailed + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; + } else { + [self sendEventsInState:RNGestureHandlerStateEnd + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:isInside + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; + } } - (void)handleTextViewTouchCancel:(UIEvent *)event From 6c761179d664467360b7f0c6d04f099a610419be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 15:54:39 +0200 Subject: [PATCH 12/19] Revert "Use correct viewTag" This reverts commit d99d12f6e9228ecbebe40365f4376c8d4a102fd6. --- .../apple/RNGestureHandlerPointerTracker.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm index 4ed6f494fb..e064d0994c 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm @@ -244,11 +244,13 @@ - (void)sendEvent // it may happen that the gesture recognizer is reset after it's been unbound from the view, // it that recognizer tried to send event, the app would crash because the target of the event // would be nil. - if (_gestureHandler.viewTag == nil && _gestureHandler.actionType != RNGestureHandlerActionTypeNativeDetector) { + if (_gestureHandler.recognizer.view.reactTag == nil && + _gestureHandler.actionType != RNGestureHandlerActionTypeNativeDetector) { return; } - [_gestureHandler sendTouchEventInState:[_gestureHandler state] forViewWithTag:_gestureHandler.viewTag]; + [_gestureHandler sendTouchEventInState:[_gestureHandler state] + forViewWithTag:_gestureHandler.recognizer.view.reactTag]; } @end From b74f00ecfb1e23bf6f917bf1708176f0a9f83b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 15:55:53 +0200 Subject: [PATCH 13/19] Simplify --- .../apple/Handlers/RNNativeViewHandler.mm | 66 +------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 75c3f67173..39f60ea25c 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -48,11 +48,6 @@ - (BOOL)isAttachedToTextView - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; - - if ([self isAttachedToTextView]) { - [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchDown:event]; - } - [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; } @@ -66,10 +61,6 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; - if ([self isAttachedToTextView]) { - [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchUp:event]; - } - self.state = UIGestureRecognizerStateFailed; // For now, we are handling only the scroll view case. @@ -83,10 +74,6 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)ev { [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event]; - if ([self isAttachedToTextView]) { - [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchCancel:event]; - } - self.state = UIGestureRecognizerStateCancelled; [self reset]; } @@ -181,9 +168,6 @@ - (void)bindToView:(UIView *)view if ([subview isKindOfClass:[UITextField class]]) { _control = (UIControl *)subview; break; - } else if ([subview isKindOfClass:[UITextView class]]) { - textInputChild = subview; - break; } } } @@ -218,14 +202,7 @@ - (void)bindToView:(UIView *)view forControlEvents:UIControlEventTouchCancel]; } } else { - // For multiline TextInput (UITextView), bind to the child view so the recognizer receives - // touch events directly, then restore viewTag to the parent's react tag. - if (textInputChild != nil) { - [super bindToView:textInputChild]; - self.viewTag = view.reactTag; - } else { - [super bindToView:view]; - } + [super bindToView:view]; } // We can restore default scrollview behaviour to delay touches to scrollview's children @@ -282,47 +259,6 @@ - (void)handleSwitch:(UIView *)sender [self reset]; } -- (void)handleTextViewTouchDown:(UIEvent *)event -{ - [self reset]; - - RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]; - - [self sendEventsInState:RNGestureHandlerStateBegan forViewWithTag:self.viewTag withExtraData:extraData]; - [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:self.viewTag withExtraData:extraData]; - _lastActiveExtraData = extraData; -} - -- (void)handleTextViewTouchUp:(UIEvent *)event -{ - BOOL isInside = [self containsPointInView]; - - if (!isInside && self.shouldCancelWhenOutside) { - [self sendEventsInState:RNGestureHandlerStateFailed - forViewWithTag:self.viewTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; - } else { - [self sendEventsInState:RNGestureHandlerStateEnd - forViewWithTag:self.viewTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:isInside - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; - } -} - -- (void)handleTextViewTouchCancel:(UIEvent *)event -{ - [self sendEventsInState:RNGestureHandlerStateCancelled - forViewWithTag:self.viewTag - withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO - withNumberOfTouches:event.allTouches.count - withPointerType:_pointerType]]; -} - - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event { [self setCurrentPointerTypeForEvent:event]; From 769f84a923bbd8fc9795d2670c310a75bac5175f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 15:58:06 +0200 Subject: [PATCH 14/19] Simplify v2 --- .../apple/Handlers/RNNativeViewHandler.mm | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 39f60ea25c..d8fe42bbec 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -17,14 +17,6 @@ #import #import -#if !TARGET_OS_OSX -@interface RNNativeViewGestureHandler () -- (void)handleTextViewTouchDown:(UIEvent *)event; -- (void)handleTextViewTouchUp:(UIEvent *)event; -- (void)handleTextViewTouchCancel:(UIEvent *)event; -@end -#endif - #pragma mark RNDummyGestureRecognizer @implementation RNDummyGestureRecognizer { @@ -154,8 +146,6 @@ - (void)updateConfig:(NSDictionary *)config - (void)bindToView:(UIView *)view { - UIView *textInputChild = nil; - // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow // for properties like `disallowInterruption` to work. if ([view isKindOfClass:[UIControl class]]) { From 9f09a7e7d609111abec721ca05ec126c3be06a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Fri, 24 Apr 2026 15:59:59 +0200 Subject: [PATCH 15/19] Simplify v3 --- .../apple/Handlers/RNNativeViewHandler.mm | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index d8fe42bbec..cd7600be14 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -32,11 +32,6 @@ - (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler } #if !TARGET_OS_OSX -- (BOOL)isAttachedToTextView -{ - return [self.view isKindOfClass:[UITextView class]]; -} - - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; @@ -52,7 +47,6 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; - self.state = UIGestureRecognizerStateFailed; // For now, we are handling only the scroll view case. @@ -65,7 +59,6 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event]; - self.state = UIGestureRecognizerStateCancelled; [self reset]; } From a79dc6c1c9cedd26931b9bc693d235a405100e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 27 Apr 2026 10:16:55 +0200 Subject: [PATCH 16/19] Revert "Simplify v3" This reverts commit 9f09a7e7d609111abec721ca05ec126c3be06a59. --- .../apple/Handlers/RNNativeViewHandler.mm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index cd7600be14..d8fe42bbec 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -32,6 +32,11 @@ - (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler } #if !TARGET_OS_OSX +- (BOOL)isAttachedToTextView +{ + return [self.view isKindOfClass:[UITextView class]]; +} + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; @@ -47,6 +52,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; + self.state = UIGestureRecognizerStateFailed; // For now, we are handling only the scroll view case. @@ -59,6 +65,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event]; + self.state = UIGestureRecognizerStateCancelled; [self reset]; } From 76df62928206d5cafb9893525d63a477dcfba187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 27 Apr 2026 10:17:12 +0200 Subject: [PATCH 17/19] Revert "Simplify v2" This reverts commit 769f84a923bbd8fc9795d2670c310a75bac5175f. --- .../apple/Handlers/RNNativeViewHandler.mm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index d8fe42bbec..39f60ea25c 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -17,6 +17,14 @@ #import #import +#if !TARGET_OS_OSX +@interface RNNativeViewGestureHandler () +- (void)handleTextViewTouchDown:(UIEvent *)event; +- (void)handleTextViewTouchUp:(UIEvent *)event; +- (void)handleTextViewTouchCancel:(UIEvent *)event; +@end +#endif + #pragma mark RNDummyGestureRecognizer @implementation RNDummyGestureRecognizer { @@ -146,6 +154,8 @@ - (void)updateConfig:(NSDictionary *)config - (void)bindToView:(UIView *)view { + UIView *textInputChild = nil; + // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow // for properties like `disallowInterruption` to work. if ([view isKindOfClass:[UIControl class]]) { From b156c8345afffd0a1cfdfc78c0fb0f4fe42bc3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 27 Apr 2026 10:17:26 +0200 Subject: [PATCH 18/19] Revert "Simplify" This reverts commit b74f00ecfb1e23bf6f917bf1708176f0a9f83b84. --- .../apple/Handlers/RNNativeViewHandler.mm | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 39f60ea25c..75c3f67173 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -48,6 +48,11 @@ - (BOOL)isAttachedToTextView - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler setCurrentPointerTypeForEvent:event]; + + if ([self isAttachedToTextView]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchDown:event]; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; } @@ -61,6 +66,10 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event]; + if ([self isAttachedToTextView]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchUp:event]; + } + self.state = UIGestureRecognizerStateFailed; // For now, we are handling only the scroll view case. @@ -74,6 +83,10 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)ev { [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event]; + if ([self isAttachedToTextView]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchCancel:event]; + } + self.state = UIGestureRecognizerStateCancelled; [self reset]; } @@ -168,6 +181,9 @@ - (void)bindToView:(UIView *)view if ([subview isKindOfClass:[UITextField class]]) { _control = (UIControl *)subview; break; + } else if ([subview isKindOfClass:[UITextView class]]) { + textInputChild = subview; + break; } } } @@ -202,7 +218,14 @@ - (void)bindToView:(UIView *)view forControlEvents:UIControlEventTouchCancel]; } } else { - [super bindToView:view]; + // For multiline TextInput (UITextView), bind to the child view so the recognizer receives + // touch events directly, then restore viewTag to the parent's react tag. + if (textInputChild != nil) { + [super bindToView:textInputChild]; + self.viewTag = view.reactTag; + } else { + [super bindToView:view]; + } } // We can restore default scrollview behaviour to delay touches to scrollview's children @@ -259,6 +282,47 @@ - (void)handleSwitch:(UIView *)sender [self reset]; } +- (void)handleTextViewTouchDown:(UIEvent *)event +{ + [self reset]; + + RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]; + + [self sendEventsInState:RNGestureHandlerStateBegan forViewWithTag:self.viewTag withExtraData:extraData]; + [self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:self.viewTag withExtraData:extraData]; + _lastActiveExtraData = extraData; +} + +- (void)handleTextViewTouchUp:(UIEvent *)event +{ + BOOL isInside = [self containsPointInView]; + + if (!isInside && self.shouldCancelWhenOutside) { + [self sendEventsInState:RNGestureHandlerStateFailed + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; + } else { + [self sendEventsInState:RNGestureHandlerStateEnd + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:isInside + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; + } +} + +- (void)handleTextViewTouchCancel:(UIEvent *)event +{ + [self sendEventsInState:RNGestureHandlerStateCancelled + forViewWithTag:self.viewTag + withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO + withNumberOfTouches:event.allTouches.count + withPointerType:_pointerType]]; +} + - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event { [self setCurrentPointerTypeForEvent:event]; From f8f903a88a37d3c203b8f914317db03a89548d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 27 Apr 2026 10:17:39 +0200 Subject: [PATCH 19/19] Reapply "Use correct viewTag" This reverts commit 6c761179d664467360b7f0c6d04f099a610419be. --- .../apple/RNGestureHandlerPointerTracker.mm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm index e064d0994c..4ed6f494fb 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandlerPointerTracker.mm @@ -244,13 +244,11 @@ - (void)sendEvent // it may happen that the gesture recognizer is reset after it's been unbound from the view, // it that recognizer tried to send event, the app would crash because the target of the event // would be nil. - if (_gestureHandler.recognizer.view.reactTag == nil && - _gestureHandler.actionType != RNGestureHandlerActionTypeNativeDetector) { + if (_gestureHandler.viewTag == nil && _gestureHandler.actionType != RNGestureHandlerActionTypeNativeDetector) { return; } - [_gestureHandler sendTouchEventInState:[_gestureHandler state] - forViewWithTag:_gestureHandler.recognizer.view.reactTag]; + [_gestureHandler sendTouchEventInState:[_gestureHandler state] forViewWithTag:_gestureHandler.viewTag]; } @end