koleS
koleS

Reputation: 1313

SwiftUI DragGesture onEnded does not fire in some cases

In my app I have a swipeable card view that can be dragged left or right.

When the user drags the card left or right, the card's horizontal offset is updated. When the user releases the drag, the card moves off the screen (either left or right, depending on the drag direction) or the card goes back to the initial position if the horizontal offset did not exceed the threshold.

It works well, but if the user touches the card view with another finger while dragging and then takes his/her fingers off screen, the card position freezes, and it doesn't either move off the screen or go back to the initial position. I debugged the code and it turns out that in that case the DragGesture().onEnded event does not fire.

enter image description here

I am looking for any hints on how I can detect this situation.

Here is the code:

If I had something like isTouchingScreen state, I would be able to solve this.

EDIT: Here is a minimal example where the problem manifests itself.

import SwiftUI



struct ComponentPlayground: View {
    @State private var isDragging: Bool = false
    @State private var horizontalOffset: CGFloat = .zero
    
    var background: Color {
        if abs(horizontalOffset) > 100 {
            return horizontalOffset < 0 ? Color.red : Color.green
        } else {
            return Color.clear
        }
    }
    
    var body: some View {
        GeometryReader { geometry in
            Color.white
                .cornerRadius(15)
                .shadow(color: Color.gray, radius: 5, x: 2, y: 2)
                .overlay(background.cornerRadius(15))
                .rotationEffect(.degrees(Double(horizontalOffset / 10)), anchor: .bottom)
                .offset(x: horizontalOffset, y: 0)
                .gesture(
                    DragGesture()
                        .onChanged { gesture in
                            self.isDragging = true
                            self.horizontalOffset = gesture.translation.width
                        }
                        .onEnded { gesture in
                            self.isDragging = false
                            
                            if abs(horizontalOffset) > 100 {
                                withAnimation {
                                    self.horizontalOffset *= 5
                                }
                            } else {
                                withAnimation {
                                    self.horizontalOffset = .zero
                                }
                            }
                            
                        }
                )
                .frame(width: 300, height: 500)
        }
    }
}

struct ComponentPlayground_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            Spacer()
            HStack(alignment: .center) {
                ComponentPlayground()
            }
            .frame(width: 300, height: 500)
            Spacer()
        }
    }
}

Upvotes: 7

Views: 2783

Answers (1)

Wilhelm Lake
Wilhelm Lake

Reputation: 75

Since there is no update on the issue from Apple, I will share my workaround for this problem. Workaround still does not provide drag functionality similar to UIPanGesture from UIKit, but still...

First I've created two GestureStates variables and keep updating them in my DragGesture:

@GestureState private var stateOffset:      CGSize = CGSize.zero
@GestureState private var isGesturePressed: Bool   = false


        let dragGesture = DragGesture()
        .updating($stateOffset) { value, gestureState, transaction in
            gestureState = CGSize(width: value.translation.width, height: value.translation.height)
        }
        .updating($isGesturePressed) { value, gestureState, transaction in
            gestureState = true
        }

By updating "isGesturePressed"(by making it true while gesture is active) I will know if the gesture is actually pressed or not. And while it pressed, I do my animation code with stateOffset:

         SomeView()
        .offset(someCustomOffset)
        .gesture(dragGesture)
        .onChange(of: stateOffset) { _ in
            if isGesturePressed {
                withAnimation { Your Animation Code here

                }
            } else {
                withAnimation { Your Animation Code here for when the gesture is no longer pressed. This is basically my .onEnd function now.

                }
            }

As I said above. It will not provide the usually UX (e.g. Twitter side menu), cause our animation just immeadiatly ends when another finger presses the screen during the on-going animation. But at least it is no longer stuck mid-way. There are definitely more elegant and better written workarounds for this, but I hope it may help someone.

Upvotes: 2

Related Questions