Skip to content

[iOS] Fix manualActivation being ignored after a config update mid-gesture - #4520

Merged
m-bert merged 2 commits into
mainfrom
@mbert/ios-manual-activation-config-reset
Sep 16, 2026
Merged

m-bert merged 2 commits into
mainfrom
@mbert/ios-manual-activation-config-reset

Conversation

@m-bert

@m-bert m-bert commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Description

On iOS a handler with manualActivation: true activates on its own if its config is updated mid-gesture. Any re-render during the gesture triggers that, since the config is re-sent on every render.

setConfig: runs resetConfig before updateConfig:, and resetConfig went through the manualActivation setter, which removed the RNManualActivationRecognizer from the view. UIKit cancels a recognizer removed mid-touch, so the failure requirement it imposed on the handler was lost and the new blocker created by updateConfig: never saw the touch.

resetConfig now only clears the flag and the blocker is reconciled once after the config is applied, only when its presence no longer matches the flag. Android and web already reset values only.

Test plan

Repro
import React, { useRef, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  GestureDetector,
  GestureStateManager,
  usePanGesture,
} from 'react-native-gesture-handler';

const ACTIVATION_THRESHOLD = 20;

type Phase = 'idle' | 'began' | 'active';

const color = (phase: Phase) =>
  phase === 'active' ? '#21a37c' : phase === 'began' ? '#f6b914' : '#9b59b6';

export default function EmptyExample() {
  const [phaseA, setPhaseA] = useState<Phase>('idle');
  const [phaseB, setPhaseB] = useState<Phase>('idle');

  const neverActivatePan = usePanGesture({
    manualActivation: true,
    runOnJS: true,
    onBegin: () => {
      console.log('[A never-activate] onBegin');
      setPhaseA('began');
    },
    onActivate: () => {
      console.log('[A never-activate] onActivate (SHOULD NOT HAPPEN)');
      setPhaseA('active');
    },
    onFinalize: (e) => {
      console.log(`[A never-activate] onFinalize canceled=${e.canceled}`);
      setPhaseA('idle');
    },
  });

  const start = useRef({ x: 0, y: 0 });
  const requested = useRef(false);

  const selfActivatePan = usePanGesture({
    manualActivation: true,
    runOnJS: true,
    onTouchesDown: (e) => {
      const t = e.allTouches[0];
      start.current = { x: t.x, y: t.y };
      requested.current = false;
    },
    onTouchesMove: (e) => {
      if (requested.current) {
        return;
      }
      const t = e.allTouches[0];
      const dx = t.x - start.current.x;
      const dy = t.y - start.current.y;
      if (Math.hypot(dx, dy) > ACTIVATION_THRESHOLD) {
        requested.current = true;
        GestureStateManager.activate(e.handlerTag);
      }
    },
    onBegin: () => {
      console.log('[B self-activate] onBegin');
      setPhaseB('began');
    },
    onActivate: () => {
      console.log('[B self-activate] onActivate');
      setPhaseB('active');
    },
    onFinalize: (e) => {
      console.log(`[B self-activate] onFinalize canceled=${e.canceled}`);
      setPhaseB('idle');
    },
  });

  return (
    <View style={styles.container}>
      <Text style={styles.label}>A: manualActivation, never activated</Text>
      <GestureDetector gesture={neverActivatePan}>
        <View style={[styles.box, { backgroundColor: color(phaseA) }]} />
      </GestureDetector>

      <Text style={styles.label}>
        B: manualActivation, activate() after {ACTIVATION_THRESHOLD}px
      </Text>
      <GestureDetector gesture={selfActivatePan}>
        <View style={[styles.box, { backgroundColor: color(phaseB) }]} />
      </GestureDetector>

      <Text style={styles.hint}>
        Purple = idle, orange = BEGAN, green = ACTIVE. Drag each box. A must
        never turn green.
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    gap: 8,
  },
  label: { marginTop: 16, fontSize: 15, opacity: 0.6 },
  hint: { marginTop: 24, fontSize: 13, opacity: 0.5, textAlign: 'center' },
  box: { width: 150, height: 150, borderRadius: 12 },
});

Copilot AI lite review requested due to automatic review settings September 15, 2026 14:15
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 06f5bf73-b17c-416f-8fd2-bdce9688bca0

📥 Commits

Reviewing files that changed from the base of the PR and between 2f688d8 and fce63ec.

📒 Files selected for processing (1)
  • packages/react-native-gesture-handler/apple/RNGestureHandler.mm

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved synchronization of manual activation settings when gesture handler configuration changes.
    • Prevented unnecessary recognizer updates when the manual activation state is already correct.
    • Ensured resetting configuration consistently disables manual activation.

Walkthrough

The Apple gesture handler now synchronizes the manual activation recognizer after configuration updates and manual activation changes. Configuration reset assigns the flag directly, while recognizer creation and removal use centralized synchronization logic.

Changes

Manual activation lifecycle

Layer / File(s) Summary
Synchronize manual activation recognizer
packages/react-native-gesture-handler/apple/RNGestureHandler.mm
resetConfig assigns _manualActivation directly. setConfig: synchronizes the recognizer after configuration updates. setManualActivation: delegates creation and removal to syncManualActivationRecognizer, which avoids redundant work.

Suggested reviewers: j-piasecki

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to fce63

The configuration update now preserves the manual activation behavior without an identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the iOS bug and the affected manualActivation behavior after a mid-gesture configuration update. It accurately summarizes the main change.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@m-bert m-bert changed the title [iOS] Fix manualActivation being ignored after a config update mid-gesture [iOS] Fix manualActivation being ignored after a config update mid-gesture Sep 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The activation blocker is preserved and synchronized correctly across configuration updates.

Pull request overview

Fixes iOS manualActivation being lost during mid-gesture configuration updates.

Changes:

  • Preserves the activation blocker during config resets.
  • Reconciles the blocker after applying configuration.
File summaries
File Description
packages/react-native-gesture-handler/apple/RNGestureHandler.mm Updates manual activation reset and blocker synchronization.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@m-bert
m-bert merged commit 186eb6b into main Sep 16, 2026
8 checks passed
@m-bert
m-bert deleted the @mbert/ios-manual-activation-config-reset branch September 16, 2026 07:55
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