Search code examples
swiftswiftuigesture-recognition

Draggable and zoomable AsyncImage does not trigger long press gesture


I have an AsyncImage that is zoomable via MagnifyGesture and drawable via a view modifier. Dragging and zooming works as expected. But now I want to show an overlay via onLongPressGesture, but this doesn't trigger. If I remove .draggable it works. How can I fix it?

public struct ContentView: View {
    
    @ObservedObject private var viewState: ViewState
    @State private var showAltText: Bool = false
    @State private var currentZoom = 0.0
    @State private var totalZoom   = 1.0
    
    public init(viewState vs:ViewState) {
        viewState = vs
    }
    public var body: some View {
        ZStack {
            VStack {
                if viewState.selectedComicStrip != nil {
                    AsyncImage(url: viewState.selectedComicStrip!.imageURL) { image in
                        image.resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(maxWidth: 400, maxHeight: 600)
                    } placeholder: {
                        Text("placeholder").frame(maxWidth: 400, maxHeight: 600)
                    }
                    .draggable()
                    .scaleEffect(currentZoom + totalZoom)
                    .gesture(
                        MagnifyGesture()
                            .onChanged { value in
                                currentZoom = value.magnification - 1
                            }
                            .onEnded { value in
                                totalZoom += currentZoom
                                currentZoom = 0
                            }
                    )
                    .onLongPressGesture {
                        showAltText.toggle()
                        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                            showAltText = false
                        }
                    }
                } else {
                    Text("placeholder")
                }
            }
        }.fullScreenCover(isPresented: $showAltText, content: {
            ZStack {
                Color.black.opacity(0.8)
                    .edgesIgnoringSafeArea(.all)
                Text(viewState.selectedComicStrip!.altText)
                    .foregroundStyle(.white)
                    .padding()
            }
        })
        .padding()
    }
}

//https://stackoverflow.com/a/63082240/106435
struct DraggableView: ViewModifier {
    @State var offset = CGPoint(x: 0, y: 0)
    
    func body(content: Content) -> some View {
        content
            .gesture(DragGesture(minimumDistance: 0)
                .onChanged { value in
                    self.offset.x += value.location.x - value.startLocation.x
                    self.offset.y += value.location.y - value.startLocation.y
                })
            .offset(x: offset.x, y: offset.y)
    }
}

extension View {
    func draggable() -> some View {
        return modifier(DraggableView())
    }
}

Solution

  • According to this Hacking with Swift post and a few others, SwiftUI will only trigger one gesture at a time by default. However, as far as I can tell from the docs, the exact behavior is undefined in general:

    When you add multiple gestures to your app’s view hierarchy, you need to decide how the gestures interact with each other. You use gesture composition to define the order SwiftUI recognizes gestures. [SwiftUI docs, emphasis mine.]

    Inspired by this SO question, you could try a couple things:

    1. Wrap the long-press gesture in a simultaneousGesture modifier. (Probably the most robust approach, suggested by the docs.)
    2. Set a positive minimumDistance on the DragGesture. (That way, it doesn't trigger immediately and preclude the long-press gesture from triggering too.)
    3. Add the onLongPressGesture before the DragGesture. (Probably the most fragile option.)