From 1ac749ed4ae39f384623de893ba5bd8fa71df38e Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Wed, 12 Aug 2026 11:52:29 +0700 Subject: [PATCH] fix(ios): hit-test nested scrollables from the touch-down point The checks deciding whether the sheet or a nested scrollable owns a pan read panGesture.location(in: sheetContainer) inside gestureRecognizerShouldBegin and handlePan(.began), i.e. after UIKit's pan translation threshold has been exceeded. That is the finger's current location, not the touch-down point, so when the draggable chrome above a scrollable is short, the threshold movement carries the point into the scrollable's frame before the check runs and the sheet refuses a drag aimed at the handle. Record the touch-down location in gestureRecognizer(_:shouldReceive:) and resolve the scrollable chain from that stored point instead, matching Android's ACTION_DOWN semantics (initialTouchX/initialTouchY). Fixes #63 --- ios/BottomSheetHostingView.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ios/BottomSheetHostingView.swift b/ios/BottomSheetHostingView.swift index 5dff2de..a88bb52 100644 --- a/ios/BottomSheetHostingView.swift +++ b/ios/BottomSheetHostingView.swift @@ -177,6 +177,7 @@ public final class BottomSheetHostingView: UIView { private var isPanning = false private var lastReportedInvalidDetentMessage: String? private var panStartingIndex: Int? + private var initialTouchLocation: CGPoint? private var activeDragRange: (minTy: CGFloat, maxTy: CGFloat)? private var activeDragDetentSpecs: [DetentSpec]? private var activeScrollViewStates: [ActiveScrollViewState] = [] @@ -881,7 +882,7 @@ public final class BottomSheetHostingView: UIView { scrollViewOwnsLowerBoundary = false activeScrollableNegotiationLevel = negotiationLevel(forVerticalVelocity: gesture.velocity(in: self).y) - let locationInContainer = gesture.location(in: sheetContainer) + let locationInContainer = initialTouchLocation ?? gesture.location(in: sheetContainer) activeScrollViewStates = scrollableAncestorChain(containing: locationInContainer).map { ActiveScrollViewState(scrollView: $0.scrollView, inverted: $0.inverted) } @@ -1227,7 +1228,7 @@ public final class BottomSheetHostingView: UIView { let candidates = snapCandidateIndices(including: targetIndex) guard candidates.count > 1 else { return false } - let locationInContainer = panGesture.location(in: sheetContainer) + let locationInContainer = initialTouchLocation ?? panGesture.location(in: sheetContainer) let scrollableChain = scrollableAncestorChain(containing: locationInContainer) guard !scrollableChain.isEmpty else { // Outside a scrollable, the sheet owns any direction in which it has a @@ -1561,6 +1562,20 @@ extension BottomSheetHostingView: UIGestureRecognizerDelegate { return true } + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldReceive touch: UITouch + ) -> Bool { + // Record the touch-down point before the pan's translation threshold is + // exceeded. gestureRecognizerShouldBegin and handlePan(.began) hit-test + // scrollables from this point instead of the post-threshold pan location, + // matching Android's ACTION_DOWN semantics. + if gestureRecognizer === panGesture, panGesture.numberOfTouches == 0 { + initialTouchLocation = touch.location(in: sheetContainer) + } + return true + } + public func gestureRecognizer( _ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy other: UIGestureRecognizer