maui icon indicating copy to clipboard operation
maui copied to clipboard

[iOS] SwipeView: Fix reenabling parent scrolling after cancelled swipe

Open filipnavara opened this issue 1 year ago • 9 comments

Description of Change

Fixes an issue where scrolling of SwipeView's parent is disabled during swipe but not reenabled when the swipe is cancelled by manually moving the touch below the threshold.

Issues Fixed

Fixes #23441 Fixes #16856

filipnavara avatar Jul 04 '24 22:07 filipnavara

Hey there @filipnavara! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

/azp run

PureWeen avatar Jul 05 '24 04:07 PureWeen

Azure Pipelines successfully started running 3 pipeline(s).

azure-pipelines[bot] avatar Jul 05 '24 04:07 azure-pipelines[bot]

I think this PR is correct, but I think this may need a nice (complicated) UI test :)

If you are feeling up to it, you can write a test emulating the human, but I can also give that a go and see who wins :)

mattleibow avatar Aug 21 '24 19:08 mattleibow

Technically the PR is probably correct with regards to the code intention.

After few iterations on our app and with our in-house testers we ended up doing something different though. We ended up fixing all iOS gesture recognisers to correctly report back state to the system and to interact with other gesture recognisers. I’ll try to write up the details tomorrow. The issue with that approach is that it changes the behaviour for minimum swipe needed to open the SwipeView (which is desirable in our case but may not be desirable in general).

filipnavara avatar Aug 21 '24 19:08 filipnavara

Sorry, took me a little while longer, but let's expand on my previous comment.

iOS uses the UIGestureRecognizer and its subclasses to implement the gesture recognition. These classes implement a state machine and a model for cooperation between multiple gesture recognizers in the UI hierarchy. Apple documentation is quite extension on how this should work: 1, 2.

Naturally, the preferred model for cooperation of multiple gesture recognizers (as is the case here with swiping and collection view scrolling) is to properly implement the state machines and UIGestureRecognizer[Delegate].ShouldBegin / UIGestureRecognizer[Delegate].ShouldRecognizeSimultaneously callbacks. This is particularly important on older iPhone models, such as iPhone SE (1st and 2nd gen), iPhone 6s, and possibly even models up to iPhone 11, which happen to have imprecise digitizer which gives back phantom touches. The native logic UIGestureRecognizer hides these tiny "phantom moves" from you.

So, how would implementing that look for MauiSwipeView? Pretty much like this diff:

  • It removes all the IsParentScrollEnabled logic to deal with CollectionView.
  • ShouldBegin recognizes horizontal swipe on the UIPanGestureRecognizer and returns true. For vertical swipe it returns false and the system automatically knows that it has to try other gesture recognizers in the hierarchy and continues with the collection view one used for scrolling.
  • ShouldRecognizeSimultaneously is no longer overridden and returns the default false value. This means that for the pan gesture either the collection view scrolling gesture recognizer will trigger, or the swipe one will, not both.

Pretty simple, right? Where's the catch? Well, there are multiple catches with this approach:

  • The swipe gesture is now recognized immediately when the user stars swiping, not respecting the MinimumOpenSwipeThresholdPercentage constant. It would likely be possible to support that with more advanced logic in the Should* gesture recognition logic. We opted not to do it because other iOS apps start the swiping gesture in collection view immediately, so this was a desired change in our case.
  • CommunityToolkit.Maui's TouchBehavior fails to implement the UIGestureRecognizer state machine which leads to all kinds of incorrect interaction between the gesture recognizers if it's used (https://github.com/CommunityToolkit/Maui/pull/2004).

I'm not quite sure how to follow up from here. We definitely should create a test, as you said. UI tests of this kind are not exactly my turf. I'd be happy to leave it up for someone else to do it, or at least point me in the right direction where a similar test already exists.

Going forward we have few options:

  • Go forward with this PR since it's relatively safe way to fix an obvious bug, even if it doesn't fix every possible issue with the pan gesture interaction.
  • Implement the UIGestureRecognizer cooperation logic correctly instead of the mess in IsParentScrollEnabled. This is relatively simple if we're okay with changing the "minimum swipe to open" behavior. It may be relatively complex if we decide to keep it.
  • The new CollectionViewHandler2 in .NET 9 branch uses compositional layout. Compositional layout has native support for swipe actions, so long term it would make sense to explore whether SwipeView inside CollectionView can be translated to the leadingSwipeActionsConfigurationProvider/trailingSwipeActionsConfigurationProvider concepts in the compositional layout.

filipnavara avatar Aug 23 '24 10:08 filipnavara

Wow! Thanks for that. I really like the idea for removing our hacks even if it loses the threshold stuff, but that is just me and we can't just break/change things. So I have pinged the team to make sure we have a chat on this.

mattleibow avatar Aug 23 '24 15:08 mattleibow

With regards to the nice fix using the ShouldBegin in your diff, how does the method args fire? If you return false for a small threshold will it fire again so we can say yes later?

For example, the line is this return Math.Abs(translation.Y) <= Math.Abs(translation.X);

Can that line be update to include a threshold check and then when it is reached we return true? Or is it too late? You said

It may be relatively complex if we decide to keep it.

What makes you think that? I am no iOS gesture expert so I don't really know how they work.

mattleibow avatar Aug 23 '24 15:08 mattleibow

With regards to the nice fix using the ShouldBegin in your diff, how does the method args fire? If you return false for a small threshold will it fire again so we can say yes later?

It only fires once, unfortunately. That means detecting a bigger threshold makes the task much more difficult since you have to start dealing with two gesture recognizers operating at the same time and then use ShouldRecognizeSimultaneously (or other mechanism) to handle the "collisions". Alternatively, it's also possible to implement the whole panning logic as a completely custom UIGestureRecognizer and simply transit the state machine between "Possible" and "Recognized" states once you made big enough swipe. I didn't really spend enough time on trying to implement this, so there may be some gotchas.

filipnavara avatar Aug 23 '24 15:08 filipnavara

/azp run

PureWeen avatar Sep 05 '24 16:09 PureWeen

Azure Pipelines successfully started running 3 pipeline(s).

azure-pipelines[bot] avatar Sep 05 '24 16:09 azure-pipelines[bot]