Skip to content

Commit

Permalink
Prioritise UITapGestureRecognizer on interop views with Cooperative
Browse files Browse the repository at this point in the history
… interop mode. Prioritise UIScreenEdgePanGestureRecognizer on ascendant views. (#1695)

Before that change taps on interop views with cooperative mode would be
discarded by compose, which expects non-discrete gesture holding on a
point before failing and redirecting touches sequence to the interop
view.

The requirement of holding ultimately made the tap gesture impossible
due to its requirement of being a short press/release sequence, which
contradicts the compose heuristics.

This scenario is fixed in this PR by allowing precedence of
`UITapGestureRecognizer` over other recognisers.

Since most of the native `tap`/`double tap` interactions are built on
top of `UITapGestureRecognizer` it should improve the UX for default
interop properties with stable constructor setting the interaction mode
to be `Cooperative`.

Coincidentally this change inspired a cheap fix for interactive pop
bugs.

Resolves https://youtrack.jetbrains.com/issue/CMP-6683
Fixes
https://youtrack.jetbrains.com/issue/CMP-6622/Gestures-dont-work-on-native-ios-view-embedded-with-UIKitView-for-compose-1.7.0-beta01

Demo:


https://github.com/user-attachments/assets/8b679ff0-6edd-42c3-9839-fe4e14cc4dff

## Testing
- Taps on interop views with cooperative interaction mode are properly
registered now and processed by native UITapGesture recogniser attached
to that view.
- Interactive pop on `UINavigationController` should not get stuck in an
awkward state preventing the gesture.

This should be tested by QA

## Release Notes
### Fixes - iOS
- Taps should be properly registered on interop views with
`UIKitInteropInteractionMode.Cooperative` interaction mode.
- Interactive pop on `UINavigationController` should recognize
correctly.
  • Loading branch information
elijah-semyonov authored Dec 11, 2024
1 parent d2ad96a commit d49b3b5
Showing 1 changed file with 51 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import platform.CoreGraphics.CGPointMake
import platform.CoreGraphics.CGRectZero
import platform.UIKit.UIEvent
import platform.UIKit.UIGestureRecognizer
import platform.UIKit.UITapGestureRecognizer
import platform.UIKit.UIScreenEdgePanGestureRecognizer
import platform.UIKit.UIGestureRecognizerDelegateProtocol
import platform.UIKit.UIGestureRecognizerState
import platform.UIKit.UIGestureRecognizerStateBegan
Expand Down Expand Up @@ -135,13 +137,19 @@ private class UserInputGestureRecognizerDelegateProxy : CMPGestureRecognizerDele
): Boolean {
// We should recognize simultaneously only with the gesture recognizers
// belonging to itself or to the views up in the hierarchy.
// An exception: UIScreenEdgePanGestureRecognizer, this always has precedence over us and is
// not allowed to recognize simultaneously

// Can't check if either view is null
// Can't proceed if either view is null
val view = gestureRecognizer.view ?: return false
val otherView = otherGestureRecognizer.view ?: return false

val otherIsAscendant = !otherView.isDescendantOfView(view)

if (otherIsAscendant && otherGestureRecognizer is UIScreenEdgePanGestureRecognizer) {
return false
}

// Only allow simultaneous recognition if the other gesture recognizer is attached to the same view
// or to a view up in the hierarchy
return otherView == view || otherIsAscendant
Expand All @@ -151,24 +159,55 @@ private class UserInputGestureRecognizerDelegateProxy : CMPGestureRecognizerDele
gestureRecognizer: UIGestureRecognizer,
otherGestureRecognizer: UIGestureRecognizer
): Boolean {
// We don't require other gesture recognizers to fail.
// Assumption is that we recognize
// simultaneously with the gesture recognizers of the views up in the hierarchy.
// And gesture recognizers down the hierarchy require to failure us.
// Two situations are possible here.
// 1. If it's a gesture recognizer of a descendant (interop) view,
// we should wait until it fails,
// if it's a UITapGestureRecognizer.
//
// 2. It's a gesture recognizer of the view itself, or it's an ascendant view.
// We don't require failure of it, unless it's a `UIScreenEdgePanGestureRecognizer`.

val view = gestureRecognizer.view ?: return false
val otherView = otherGestureRecognizer.view ?: return false

val otherIsDescendant = otherView.isDescendantOfView(view)
val otherIsAscendantOrSameView = !otherIsDescendant

// (1)
if (otherIsDescendant && otherGestureRecognizer is UITapGestureRecognizer) {
return true
}

// (2)
if (otherIsAscendantOrSameView && otherGestureRecognizer is UIScreenEdgePanGestureRecognizer) {
return true
}

return false
}

override fun gestureRecognizerShouldBeRequiredToFailByGestureRecognizer(
gestureRecognizer: UIGestureRecognizer,
otherGestureRecognizer: UIGestureRecognizer
): Boolean {
// Other gesture recognizers,
// except the case where it belongs to the same view,
// are required to wait until we fail.
// In practice, it can only happen when other gesture recognizers are attached to the
// descendant views (aka interop views).
// In other cases, it's allowed to recognize simultaneously, so this method will not be
// called

// otherGestureRecognizer is UITapGestureRecognizer,
// it must not wait till we fail and has priority
if (otherGestureRecognizer is UITapGestureRecognizer) {
return false
}

val view = gestureRecognizer.view ?: return false
val otherView = otherGestureRecognizer.view ?: return false

val otherIsDescendant = otherView.isDescendantOfView(view)
val otherIsAscendantOrSameView = !otherIsDescendant

if (otherIsAscendantOrSameView && otherGestureRecognizer is UIScreenEdgePanGestureRecognizer) {
return false
}

// Otherwise it is required to fail (aka other kind of gesture recognizer on interop view)
return gestureRecognizer.view != otherGestureRecognizer.view
}
}
Expand Down

0 comments on commit d49b3b5

Please sign in to comment.