diff --git a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm index 5e67f54326..75c3f67173 100644 --- a/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm +++ b/packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm @@ -14,8 +14,17 @@ #import #import +#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 { @@ -31,9 +40,19 @@ - (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]; + + if ([self isAttachedToTextView]) { + [(RNNativeViewGestureHandler *)_gestureHandler handleTextViewTouchDown:event]; + } + [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event]; } @@ -46,6 +65,11 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - (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. @@ -58,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]; } @@ -114,6 +143,9 @@ @implementation RNNativeViewGestureHandler { BOOL _shouldActivateOnStart; BOOL _disallowInterruption; RNGestureHandlerEventExtraData *_lastActiveExtraData; +#if !TARGET_OS_OSX + __weak UIControl *_control; +#endif } - (instancetype)initWithTag:(NSNumber *)tag @@ -135,39 +167,65 @@ - (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]]) { - UIControl *control = (UIControl *)view; + _control = (UIControl *)view; + } else if ([view isKindOfClass:[RCTTextInputComponentView class]]) { + // 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]]) { + _control = (UIControl *)subview; + break; + } else if ([subview isKindOfClass:[UITextView class]]) { + textInputChild = 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]; + [_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]; + // 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 @@ -181,8 +239,9 @@ - (void)unbindFromView { UIView *view = self.recognizer.view; - if ([view isKindOfClass:[UIControl class]]) { - [(UIControl *)view removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; + if (_control) { + [_control removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents]; + _control = nil; } // Restore the React Native's overriden behavor for not delaying content touches @@ -223,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]; 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