Skip to content

[ios] not calling activation callback on inactive gestures#3986

Merged
akwasniewski merged 21 commits intomainfrom
@akwasniewski/ios-fix-state-manager-begin
Mar 5, 2026
Merged

[ios] not calling activation callback on inactive gestures#3986
akwasniewski merged 21 commits intomainfrom
@akwasniewski/ios-fix-state-manager-begin

Conversation

@akwasniewski
Copy link
Contributor

Description

Gestures always call activation callback upon finalizing, regardless of the previous state. This is intended for normal gestures, however for manually handled gestures we don't want to call it. This PR unifies the behaviour across platforms, GestureManager.finalize() won't call activation callback.

Test plan

Tested on the state manager example

Copy link
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

This PR fixes an issue where gestures were always calling the activation callback upon finalizing, regardless of their previous state. The fix ensures that manually handled gestures don't trigger the activation callback, unifying behavior across iOS and other platforms.

Changes:

  • Added a manualActivation parameter throughout the gesture handling call chain to distinguish manual state changes
  • Modified sendEventsInState to check both the parameter and instance variable before sending activation callbacks
  • Updated all gesture recognizer subclasses that override handleGesture:fromReset: to support the new parameter

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
RNGestureHandlerModule.mm Pass manualActivation:YES when manually setting gesture state via setGestureStateSync
RNGestureHandler.mm Add manualActivation parameter to gesture handling methods and update activation callback logic
RNGestureHandler.h Declare new method signatures with manualActivation parameter
RNRotationHandler.m Add overloaded handleGesture:fromReset:manualActivation: method
RNPinchHandler.m Add overloaded handleGesture:fromReset:manualActivation: method
RNLongPressHandler.m Add overloaded handleGesture:fromReset:manualActivation: method with platform-specific declarations
RNFlingHandler.m Add overloaded handleGesture:fromReset:manualActivation: method for macOS version

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

Copy link
Contributor

@m-bert m-bert left a comment

Choose a reason for hiding this comment

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

Could you please merge recent updates? There were changes to handleGesture that should be considered in this PR

Base automatically changed from @akwasniewski/ios-state-manager-dont-rely-on-reset to main February 23, 2026 07:14
@akwasniewski akwasniewski requested a review from Copilot February 23, 2026 07:37
Comment on lines +143 to +144
[_gestureHandler handleGesture:self fromReset:fromReset manualActivation:NO];
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this override needed at all? It doesn't change anything compared to the base implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes we need the default value one so that the selector works correctly (as in #3983) and we need to overload the one with all the arguements so that when called from stateManager it initializes the handler specific values (like previousTime in LongPress or scale in pinch).

Copy link
Member

Choose a reason for hiding this comment

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

In the PR you linked, the base one ((void)handleGesture:(nonnull id)recognizer;) is implemented once in the base class. Here, handlers have both - the base one, and the extended one - defined. The base one is exactly the same as the implementation in GestureHandler. Hover doesn't seem to override either, and I assume the selector works there, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah right, I assume the override in Fling was used at some, but not anumore. Removed altogether in 1246b9d. The other overrides are needed though.

Copy link
Member

Choose a reason for hiding this comment

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

Why are they needed? They don't differ from the default implementation.

Copy link
Contributor Author

@akwasniewski akwasniewski Mar 2, 2026

Choose a reason for hiding this comment

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

Without them corresponding gestures crash on activation

Copy link
Member

Choose a reason for hiding this comment

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

Ok, i see - this method is defined in the recognizer not the handler. I'm not sure whether that's right - can you try moving them to the handler?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct me if I am wrong, but it's by design. In those few gestures it is defined in the recognizer (rather than the handler) as it sets some recognizer fields.

Copy link
Member

Choose a reason for hiding this comment

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

Possibly, but it's confusing as hell when they share the exact same definitions and handler/recognizer is in the same file.

Can you look into refactoring that (after this PR)?

Comment on lines +416 to +418
} else if (
state == RNGestureHandlerStateEnd && _lastState != RNGestureHandlerStateActive && !manualActivation &&
!_manualActivation) {
Copy link
Member

Choose a reason for hiding this comment

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

What's the end result here? onFinalize is invoked with didSucceed: true without going through activate/deactivate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this unifies the behaviour across platforms. Previously we forced calling activation callback. Do we want to handle it differently?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think if the user called onFinalize, rather than onFail he wants to have the onSuccess flag set to true (for whaterver reason), but if he wanted the activation callback he would have called StateManager.activate directly.

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by

I think if the user called onFinalize, rather than onFail

I don't recall having onFail in that context.

Copy link
Contributor Author

@akwasniewski akwasniewski Feb 23, 2026

Choose a reason for hiding this comment

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

Sorry, I meant when they called GestureStateManager.finalize() rather than GestureStateManager,fail()

Copy link
Member

Choose a reason for hiding this comment

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

Is didSucceed: true consistent across platforms here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's currently s the same on web, it's false on android. I will change it on android as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just noticed that android finalize works yet again differently than on other platforms, I'll have to take a good look at it, thus android changes will be done in a seperate pr

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PR which syncs android #4011. The logic is in fact the same, the didSucceed flag is set to true there, however there was an issue that canceled manually activated gestures.

Copy link
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 7 out of 7 changed files in this pull request and generated 2 comments.


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

Copy link
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 10 out of 10 changed files in this pull request and generated 2 comments.


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

{
previousTime = CACurrentMediaTime();
[_gestureHandler handleGesture:recognizer fromReset:NO];
[_gestureHandler handleGesture:recognizer fromReset:NO manualActivation:NO];
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

previousTime is no longer updated in the handleGesture: action method, but getDuration relies on previousTime - startTime. Since handleGesture: is the selector registered as the recognizer's action, duration may stay near 0 or stale for the whole gesture. Consider updating previousTime here as well (or route handleGesture: through the fromReset:manualActivation: path that sets it).

Suggested change
[_gestureHandler handleGesture:recognizer fromReset:NO manualActivation:NO];
[self handleGesture:recognizer fromReset:NO manualActivation:NO];

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
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 10 out of 10 changed files in this pull request and generated 2 comments.


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

Comment on lines 138 to 143
return self;
}

- (void)handleGesture:(NSPanGestureRecognizer *)gestureRecognizer
{
[_gestureHandler handleGesture:self];
}

- (void)mouseDown:(NSEvent *)event
{
[super mouseDown:event];
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

In the macOS implementation of RNBetterSwipeGestureRecognizer, the recognizer is still created with initWithTarget:self action:@selector(handleGesture:), but handleGesture: is no longer present (it was removed in this change). When AppKit fires the action, this will crash with an unrecognized selector. Please re-add -handleGesture: to forward to _gestureHandler, or change the target/action to _gestureHandler like the iOS version.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Copilot AI commented Feb 25, 2026

@m-bert I've opened a new pull request, #3996, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits February 25, 2026 13:04
…dler API (#3996)

The `fromManual:` selector label was ambiguous — it could be confused
with the existing `manualActivation` flag on `RNGestureHandler`. Rename
it to `fromManualStateChange:` to clearly convey that this parameter
tracks state changes triggered via `setGestureState` (the manual state
manager path), not manual activation.

- **`RNGestureHandler.h`** — updated all three method declarations
- **`RNGestureHandler.mm`** — updated implementations and all call sites
- **`RNGestureHandlerModule.mm`** — updated `setGestureState` call sites
(the only places passing `YES`)
- **All handler files** (`RNFlingHandler`, `RNHoverHandler`,
`RNLongPressHandler`, `RNPanHandler`, `RNPinchHandler`,
`RNRotationHandler`, `RNTapHandler`) — updated forwarding declarations
and call sites

<!-- START COPILOT CODING AGENT TIPS -->
---

🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Copy link
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 10 out of 10 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 427 to 431
// the same coalescing-key allowing RCTEventDispatcher to coalesce RNGestureHandlerEvents when events are
// generated faster than they can be treated by JS thread
static uint16_t nextEventCoalescingKey = 0;
self->_eventCoalescingKey = nextEventCoalescingKey++;

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The new state == END && _lastState != ACTIVE branch now returns early when !fromManualStateChange || !_manualActivation, which means (1) normal recognizers that end without ever reaching ACTIVE (e.g., discrete gestures) will stop emitting the END state-change event entirely, and (2) manually-state-changed gestures with _manualActivation == NO will also skip END (so onFinalize won’t fire). If the intent is only to avoid emitting a synthetic ACTIVE before END for manual state changes, remove the early return and instead gate only the synthetic-ACTIVE injection (e.g., keep existing behavior for non-manual changes, but skip injecting ACTIVE when fromManualStateChange is true).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@akwasniewski akwasniewski force-pushed the @akwasniewski/ios-fix-state-manager-begin branch from 0e49254 to 54523da Compare March 2, 2026 19:35
Copy link
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 10 out of 10 changed files in this pull request and generated 1 comment.


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

@akwasniewski akwasniewski force-pushed the @akwasniewski/ios-fix-state-manager-begin branch from fd2cea1 to 12bbf7f Compare March 3, 2026 10:47
Comment on lines +143 to +144
[_gestureHandler handleGesture:self fromReset:fromReset manualActivation:NO];
}
Copy link
Member

Choose a reason for hiding this comment

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

Possibly, but it's confusing as hell when they share the exact same definitions and handler/recognizer is in the same file.

Can you look into refactoring that (after this PR)?

@akwasniewski akwasniewski merged commit af8b121 into main Mar 5, 2026
@akwasniewski akwasniewski deleted the @akwasniewski/ios-fix-state-manager-begin branch March 5, 2026 17:56
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.

5 participants