SwiftUI: DragGesture in ScrollView doesn't register scrolling when swipe begins on DragGesture view

Viewed 2195

So I have this really nice animation I want when a user presses a view, where it will sort of "bounce" under their finger as if it reacted to the touch.

Initial Goal

I need to register when the user presses down on the view (black rectangle in this example), so I can start shrinking it, then I need to register when they release on the view, so I can allow it to return to it's original size. User needs to still be able to scroll normally in ScrollView

Initial "Solution"

I managed to achieve the desired animation using DragGesture() gesture with minimumDistance: 0.

See Example GIF here

This is achieved with the following code:

@State private var pressed: Bool = false

var body: some View {
        let drag = DragGesture(minimumDistance: 0)
            .onChanged({_ in
                self.pressed = true
            })
            .onEnded({_ in
                self.pressed = false
            })
        
        return ScrollView {
            Rectangle()
                .frame(minHeight: 100)
                .scaleEffect(self.pressed ? 0.5 : 1)
                .animation(.easeInOut)
//                .onTapGesture(perform: {
//                    self.pressed.toggle()
//                })
                .gesture(drag)
        }
    }

Problem

If you user begins to scroll by starting with their finger on the rectangle, it will register as a drag gesture. If they begin scrolling elsewhere, scrolling is as normal

See Example GIF here

Alternative

If I use the modifier .onTapGesture() then I can tap and it's registered, and also begin scrolling and it registers as a scroll as appropriate

See Example GIF here

This is achieved with the following code:

@State private var pressed: Bool = false
    
    var body: some View {
        let drag = DragGesture(minimumDistance: 0)
            .onChanged({_ in
                self.pressed = true
            })
            .onEnded({_ in
                self.pressed = false
            })
        
        return ScrollView {
            Rectangle()
                .frame(minHeight: 100)
                .scaleEffect(self.pressed ? 0.5 : 1)
                .animation(.easeInOut)
                .onTapGesture(perform: {
                    self.pressed.toggle()
                })
//                .gesture(drag)
        }
    }

Question

So how can I still get the desired animation whilst allowing scrolling? I've looked into GestureMasks, simultaneousGesture, LongPressGesture and more and they don't achieve the quick responsive animation I'm looking for...

Thanks!

1 Answers

Managed to "mostly" fix it myself, posting answer to help others.

It's not perfect, but much better. If anyone can figure out how to make it not scale down when scrolling on it that would be great. But otherwise you can tap it and scroll on it just fine.

struct ContentView: View {
    @State private var pressed: Bool = false

    var body: some View {
        print(self.pressed)
        return ScrollView {
            Button(action: { self.pressed.toggle() }) {
                Rectangle()
                    .frame(minHeight: 100)
                    .animation(.easeInOut)
            }.buttonStyle(tapBounceButtonStyle())
        }
    }
}

struct tapBounceButtonStyle: ButtonStyle {
  func makeBody(configuration: Self.Configuration) -> some View {
    configuration.label
        .scaleEffect(configuration.isPressed ? 0.9 : 1)
  }
}
Related