Skip to content

[iOS] Keep button hit testing within its own subtree - #4495

Open
rileysay wants to merge 1 commit into
software-mansion:mainfrom
rileysay:patch-1
Open

[iOS] Keep button hit testing within its own subtree#4495
rileysay wants to merge 1 commit into
software-mansion:mainfrom
rileysay:patch-1

Conversation

@rileysay

@rileysay rileysay commented Sep 8, 2026

Copy link
Copy Markdown

Description

Fixes #4494.

When a disabled RNGestureHandlerButton has no eligible touch target, the hit-test loop can continue past the button and return an ancestor outside its subtree.

This change returns nil when the search reaches the button itself without finding an eligible target. Eligible descendants can still be selected before reaching that boundary.

The guard prevents the SwiftUI-backed blur crash in my device testing. The suspected connection between returning a hosting ancestor and recursive hit testing is described in the linked issue.

Test plan

Tested the same boundary guard in my app’s test screen on an iPhone 16 Pro running iOS 26.5.2, using an Expo SDK 57 development build with React Native 0.86.3 and Gesture Handler 3.2.1.

React Native’s experimental enableSwiftUIBasedFilters flag was enabled.

  • Original behaviour: tapping the disabled outer area crashes.
  • Boundary guard enabled, pointer-events workaround off: tapping the same area no longer crashes.

Reproduction project and build instructions:
https://github.com/rileysay/rngh-swiftui-hit-test-repro

The standalone project has built successfully on EAS. The device observations above were recorded using the test screen in my original app.

AI assistance: I used Codex to help investigate the native code, prepare the reproduction project, and propose this patch. I personally verified the device observations above.

Stop the upward hit-test search at the button when no eligible target has been found. This prevents returning an ancestor outside the button’s subtree while keeping eligible descendants reachable.
Copilot AI lite review requested due to automatic review settings September 8, 2026 05:11
@coderabbitai

coderabbitai Bot commented Sep 8, 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: 137ffd64-eb57-49f7-841d-ae32e553951f

📥 Commits

Reviewing files that changed from the base of the PR and between ebc923b and 826f32e.

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

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


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved touch handling for gesture buttons on Apple platforms.
    • Prevented buttons from being selected as touch targets when they do not qualify to handle the touch.

Walkthrough

The iOS button hit-test loop now stops at the button boundary when the button rejects a touch. This prevents traversal into ancestor views and avoids recursive hit testing in SwiftUI-backed view hierarchies.

Changes

iOS hit testing

Layer / File(s) Summary
Stop rejected hit-test traversal
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm
hitTest:withEvent: returns nil when the candidate view is the button itself and shouldHandleTouch: rejects the touch.

Suggested reviewers: j-piasecki, m-bert

Priority: ➖ Normal — Impact reflects medium issue severity.

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 826f3

Disabled button areas now stop hit testing at the button boundary, preventing ancestor traversal and the reported crash while retaining eligible nested controls. No current merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: preventing button hit testing from traversing outside the button's subtree on iOS.
Linked Issues check ✅ Passed The change directly addresses issue #4494 by stopping hit testing at the disabled button boundary, returning nil when no eligible target exists, and preserving eligible nested child controls.
Out of Scope Changes check ✅ Passed The changes are limited to the hit-test boundary guard in RNGestureHandlerButton.mm and support the linked issue objectives. No unrelated changes are present.

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.

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 change is small, well-scoped to disabled-button hit testing, and the new guard prevents returning out-of-subtree ancestors without affecting eligible descendant selection.

Pull request overview

Fixes an iOS hit-testing edge case in RNGestureHandlerButton where, when the button is disabled and no eligible descendant view is found, the hit-test search could continue past the button and incorrectly return an ancestor outside the button’s subtree (which can contribute to recursive hit testing / SwiftUI-hosting-related crashes as described in #4494).

Changes:

  • Add a boundary guard in -hitTest:withEvent: to stop the “walk up superviews” loop at the button itself and return nil instead of returning an ancestor outside the subtree.
File summaries
File Description
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm Stops the hit-test eligibility search from walking past a disabled RNGestureHandlerButton into ancestor views by returning nil when the search reaches the button itself.
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 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @rileysay! Thank you for this PR!

The change seems ok, but there's one problem - it now allows siblings of disabled buttons to take over the touch, even though they shouldn't.

Here is the example of the failing structure
 <View style={{ height: 120 }}>
	<Touchable
	  onPress={() => console.log('outer')}
	  style={{
	    position: 'absolute',
	    inset: 0,
	    backgroundColor: '#9fd8a8',
	  }}
	/>
	<Touchable
	  disabled
	  style={{
	    position: 'absolute',
	    left: 40,
	    top: 20,
	    width: 200,
	    height: 80,
	    backgroundColor: '#9a9a9a',
	  }}
	/>
</View>

To fix this, we can slightly change the approach:

  1. Return self instead of nil, so that button still can become a touch target:
- while (inner && ![self shouldHandleTouch:inner atPoint:point]) {
+ while (inner && inner != self && ![self shouldHandleTouch:inner atPoint:point]) {
  1. Early return from beginTrackingWithTouch:withEvent: when button is disabled:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
  if (!_userEnabled) {
    return NO;
  }
  ...
  1. Early return from point 2 should also be applied to mouseDown, mouseUp and mouseDragged methods to keep macOS behavior correct.

After that everything should work fine 😅

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.

[iOS] Disabled Touchable crashes inside a SwiftUI-backed React Native blur view

3 participants