fermoya / SwiftUIPager

Native Pager in SwiftUI
MIT License
1.27k stars 168 forks source link

The pager gets stuck between two pages when adding a second touch when swiping #238

Closed kevmusic closed 2 years ago

kevmusic commented 2 years ago

Steps to reproduce

  1. With one finger, start swiping up until half of the next item is displayed
  2. Without releasing the first finger, touch the second video with another finger
  3. Release both fingers at the same time

https://user-images.githubusercontent.com/13948320/139624570-0529c135-0ba2-429f-8595-d12fe72692b8.mp4

fermoya commented 2 years ago

hi @kevmusic . Yes, I remember something similar to this when you used a second finger to pull-down the Notification Center. SwiftUI doesn't call the onEnded callback inside DragGesture but I'll see if there's any workaround to this

kevmusic commented 2 years ago

Hi @fermoya!

Were you able to find a workaround?

Let me know, thanks!

fermoya commented 2 years ago

Hi @kevmusic , unfortunately there's been no luck. I found a possible solution where I can update and observe changes in a GestureState. According to this answer, the onEnded method wouldn't be called but GestureState should reset. I tried this but unfortunately it seems to not work as they say. I reported a bug but I've gotten no answer so far.

Here's a simple example that reproduces it:

import SwiftUI

struct ContentView: View {
  @GestureState var offset: CGSize = .zero

  var body: some View {
    Text("Hello, world!")
      .offset(offset)
      .gesture(
        DragGesture()
          .updating($offset, body: { value, state, _ in
            state = value.translation
          })
      )
  }
}

It doesn't seem to happen as often, but it still happens:

https://user-images.githubusercontent.com/11335612/151333053-75564be1-f475-4f90-822e-36f4f99ff245.mov

darecki commented 2 years ago

It's very easy to be reproduced. Just tap with another finger while dragging between elements. See the video:

https://user-images.githubusercontent.com/3084606/160623643-b8091ec7-39a7-46a8-b0b8-def05d372a9d.mov

What is worse, changing pages with page.update(.next) doesn't reset the offset. The only way to reset the offset is to drag again. It can't be done programmatically. I implemented this Reset button which does it, but it uses some internal APIs. It's actually the onDragCancelled() function from PagerContent.

HStack {
    Button("Prev") {
        withAnimation {
            page2.update(.previous)
        }
    }
    Button("Reset") {
        withAnimation {
            page2.draggingOffset = 0
            page2.lastDraggingValue = nil
            page2.draggingVelocity = 0
            page2.objectWillChange.send()
        }
    }
    Button("Next") {
        withAnimation {
            page2.update(.next)
        }
    }
}

https://user-images.githubusercontent.com/3084606/160625979-4681b703-826e-403c-b9ec-8e692680c122.mp4

Question: can you make the onDragCancelled() function publicly available via the Page object? So that it could be called this way page.reset()? This way I could implement some workarounds on my side.

fermoya commented 2 years ago

@darecki you can't page.reset() because what's the initial page? For some might 0, for some other people it might be different.

I don't think onDragCancelled gets called. That's there for this specific case scenario #69 . In this issue, it doesn't work as the App is still active. The problem is the same, but #69 had a workaround

darecki commented 2 years ago

@fermoya probably name reset() that I proposed was a bit unfortunate and you misunderstood my intentions. This function, whatever is named, is supposed to reset the dragging state, which is exactly what you do in onDragCancelled. I want to be able to call onDragCancelled from the outside, which I indirectly did in this code snippet

    Button("Reset") {
        withAnimation {
            page2.draggingOffset = 0
            page2.lastDraggingValue = nil
            page2.draggingVelocity = 0
            page2.objectWillChange.send()
        }
    }

That is not possible now, as this API is internal and not public. I hope it is more clear now.

darecki commented 2 years ago

If I had this possibility to reset the carousel from my code, I could work around the problem this way:

Set up a new timer on each onDraggingChanged (say 0.5 second). Invalidate the timer on onDraggingEnded If timer fires == no onDraggingEnded callback received -> I assume the view got stuck and I programmatically reset the view.

As of now, I unfortunately can't do it this way.

fermoya commented 2 years ago

@kevmusic @darecki apologies because this is something I've been testing in a simulator and not on a real device. Somehow I get different behaviors.

I've made a change that I think solves this issue. @darecki do you mind confirming this? It should be fixed in 2.3.3-beta.5

darecki commented 2 years ago

@fermoya yes, it fixes the issue on my end. Great job!