Skip to content

[iOS] TextInput callbacks#4114

Open
m-bert wants to merge 16 commits intomainfrom
@mbert/textinput
Open

[iOS] TextInput callbacks#4114
m-bert wants to merge 16 commits intomainfrom
@mbert/textinput

Conversation

@m-bert
Copy link
Copy Markdown
Collaborator

@m-bert m-bert commented Apr 23, 2026

Description

I've noticed that on iOS none of the gesture callbacks is triggered when using our TextInput. This works fine on Android and web.

When we attach handler it is not attached to UITextField nor to UITextView. Instead it is attached to RCTTextInputComponentView. Because of that we do not receive any up/down events.

Test plan

Tested on the following code:
import { useState } from 'react';
import { StyleSheet, TextInput } from 'react-native';
import {
  GestureDetector,
  GestureHandlerRootView,
  TextInput as RNGHTextInput,
  useNativeGesture,
} from 'react-native-gesture-handler';

export default function App() {
  const [value, setValue] = useState('');

  const g = useNativeGesture({
    onBegin: () => {
      console.log('gesture begin');
    },
    onActivate: () => {
      console.log('gesture activate');
    },
    onUpdate: () => {
      console.log('gesture update');
    },
    onDeactivate: () => {
      console.log('gesture deactivate');
    },
    onFinalize: () => {
      console.log('gesture finalize');
    },
  });

  return (
    <GestureHandlerRootView style={styles.container}>
      <TextInput
        value={value}
        onChangeText={setValue}
        style={[styles.input, { backgroundColor: 'red' }]}
      />
      <GestureDetector gesture={g}>
        <TextInput
          value={value}
          onChangeText={setValue}
          style={[styles.input, { backgroundColor: 'green' }]}
        />
      </GestureDetector>
      <RNGHTextInput
        onBegin={() => {
          console.log('gesture begin');
        }}
        onActivate={() => {
          console.log('gesture activate');
        }}
        onUpdate={() => {
          console.log('gesture update');
        }}
        onDeactivate={() => {
          console.log('gesture deactivate');
        }}
        onFinalize={() => {
          console.log('gesture finalize');
        }}
        style={[styles.input, { backgroundColor: 'blue' }]}
        value={value}
        onChangeText={setValue}
      />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {
    width: 200,
    height: 50,
  },
});

Copilot AI review requested due to automatic review settings April 23, 2026 14:47
@m-bert m-bert marked this pull request as ready for review April 23, 2026 14:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes iOS TextInput gesture callbacks not firing by adjusting how RNNativeViewGestureHandler binds to React Native TextInput views.

Changes:

  • Import RCTTextInputComponentView and detect it in bindToView:.
  • Attempt to locate the backing UITextField/UITextView subview and treat it as a UIControl target for touch event callbacks.
  • Track and unbind the resolved control via a stored _control reference.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Outdated
@m-bert m-bert marked this pull request as draft April 23, 2026 15:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +215 to +217
// touch events directly, then restore viewTag to the parent's react tag.
if (textInputChild != nil) {
[super bindToView:textInputChild];
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When binding multiline TextInput, the recognizer is attached to textInputChild but only self.viewTag is restored to the parent (view.reactTag). Touch callbacks that rely on needsPointerData (e.g. onTouchesDown/Move/Up/Cancelled) are dispatched by RNGestureHandlerPointerTracker using gestureHandler.recognizer.view.reactTag, so they’ll use the child UITextView tag (often nil) instead of the TextInput’s tag and won’t reach JS. Consider also aligning the event target for pointer-tracker events (e.g., ensure the bound child view has the parent reactTag for event dispatching, or change the native-view handler’s pointer-event dispatch path to use self.viewTag / findViewForEvents rather than recognizer.view.reactTag).

Suggested change
// touch events directly, then restore viewTag to the parent's react tag.
if (textInputChild != nil) {
[super bindToView:textInputChild];
// touch events directly, then align both the handler's viewTag and the bound child's reactTag
// with the parent's react tag so pointer-tracker events are dispatched to the correct JS target.
if (textInputChild != nil) {
[super bindToView:textInputChild];
textInputChild.reactTag = view.reactTag;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d99d12f should be enough

Comment thread packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Outdated
Comment thread packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Outdated
Comment thread packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Outdated
m-bert and others added 5 commits April 24, 2026 12:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


[_gestureHandler sendTouchEventInState:[_gestureHandler state]
forViewWithTag:_gestureHandler.recognizer.view.reactTag];
[_gestureHandler sendTouchEventInState:[_gestureHandler state] forViewWithTag:_gestureHandler.viewTag];
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendEvent forwards pointer events using [_gestureHandler state], but some handlers (notably RNNativeViewGestureHandler in the UITextView path) manually dispatch BEGAN/ACTIVE via sendEventsInState without updating the internal _state. In that case, touch/pointer events will be emitted with state=BEGAN even after activation, which is inconsistent with other handlers and can confuse JS consumers that read event.state. Consider deriving the state for touch events from gestureHandler.lastState when available (falling back to state only when lastState is still UNDTERMINED).

Suggested change
[_gestureHandler sendTouchEventInState:[_gestureHandler state] forViewWithTag:_gestureHandler.viewTag];
RNGestureHandlerState eventState = [_gestureHandler lastState];
if (eventState == RNGestureHandlerStateUndetermined) {
eventState = [_gestureHandler state];
}
[_gestureHandler sendTouchEventInState:eventState forViewWithTag:_gestureHandler.viewTag];

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v3 consumers won't read event.state. Also Native gesture differs based on underlying view. IMO we can leave that since callbacks are ok (cc @j-piasecki)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We also have v1 and v1 API available in the package
  2. Native differes based on view/platform but I'd say sending random BEGIN state change doesn't fall into that

Comment on lines +300 to +314
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]];
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When shouldCancelWhenOutside is enabled and the touch ends outside, this path dispatches RNGestureHandlerStateFailed. For single-line TextInput/UIControl-backed views, leaving the bounds results in a cancellation flow (UIControlEventTouchCancelCANCELLED). To keep behavior consistent across single-line and multiline TextInput, consider dispatching CANCELLED here (and setting pointerInside accordingly) rather than FAILED.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters in that case. I've changed it to failed purposely. I can bring back Cancelled, but I don't think it makes much of a difference.

@m-bert m-bert requested a review from j-piasecki April 24, 2026 11:24
@m-bert m-bert marked this pull request as ready for review April 24, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants