skip to Main Content

I have a fulscreen SpriteKit scene view, that handles pan gestures within itself.
Over it, I have overlaid native SwiftUI views with OnTapGesture, which must react to them being touched but should not interfere with pan gestures, and pass them below.

.allowHitTesting(false) completely removes any reaction to touches, pan gestures work but button handlers won’t.

I’ve written a small example that recreates my situation:

import SwiftUI

struct TestView: View
{
    var body: some View
    {
        ZStack
        {
            Rectangle() //simulates spriteKit view
                .fill(.blue)
                .ignoresSafeArea()
                .simultaneousGesture(DragGesture().onChanged(
                    { value in
                        //simulates spriteKit drag handling,
                        //for example animated page flipping
                        print("DragGesture",value.velocity)
                    } ))
                
            HStack(spacing: 0) //my buttons
            {
                Rectangle()
                    .fill(.red.opacity(0.00001))
                    .border(.red, width: 2)
                    .onTapGesture
                    {
                        // this one gets called,
                        // but sk drag gesture is NOT called
                        print("flip page left")
                    }
                
                Rectangle()
                    .fill(.clear)
                    .border(.green, width: 2)
                    .onTapGesture
                    {
                        // this one is NOT called,
                        // and sk gesture is called
                        print("flip page right")
                    }
                
                Button(action:
                        {
                            // this one gets called,
                            // but sk drag gesture is NOT called
                            print("button PRESSED")
                        },
                       label:
                        {
                            Rectangle()
                                .fill(.black)
                                .border(.yellow, width: 2)
                        })
            }
        }
    }
}


#Preview {
    TestView()
}

So, to reiterate: How can I make TRANSPARENT (or 0.0001 opacity) overlaying buttons that capture single touches but can pass pan (or drag) gestures to the views below them?

2

Answers


  1. In my experience, it’s best to define the gestures separately and then use them as needed. This will offer more flexibility for using them with composing gestures modifiers like .simultaneously, .exclusively and .sequenced, but also with GestureMask parameters.

    Here’s the revised code for you to try:

    import SwiftUI
    
    struct DragTapTestView: View {
        
        //Gestures
        let dragGesture = DragGesture(minimumDistance: 3)
            .onChanged { value in
                //simulates spriteKit drag handling,
                //for example animated page flipping
                print("DragGesture",value.velocity)
            }
        
        let flipLeftTap =  TapGesture()
            .onEnded { _ in
                print("flip page left")
            }
        
        let flipRightTap =  TapGesture()
            .onEnded { _ in
                print("flip page right")
            }
        
        //Body
        var body: some View {
            ZStack {
                
                //Background with drag gesture
                Rectangle() //simulates spriteKit view
                    .fill(.blue)
                    .ignoresSafeArea()
                    .gesture(dragGesture)
                
                //Buttons
                HStack(spacing: 0) {
                    
                    //Flip Left
                    Color.clear
                        .border(.red, width: 2)
                        .contentShape(Rectangle())
                        .gesture(
                            flipLeftTap
                                .simultaneously(with: dragGesture) //Note this could trigger both a drag and a tap, depending on drag distance and timing
                        )
                    
                    //Spacer
                    Color.clear
                        .border(.green, width: 2)
                    
                    VStack(spacing: 0) {
                        
                        //Flip Right
                        Color.clear
                            .border(.yellow, width: 2)
                            .contentShape(Rectangle())
                            .gesture(
                                dragGesture
                                    .exclusively(before: flipRightTap) //more reliable - compare this with the left tap approach
                            )
                        
                        //Flip Right - Alternate method using a button
                        Button {
                            print("flip right button PRESSED")
                        } label: {
                            Color.clear
                                .border(.orange, width: 2)
                        }
                        .highPriorityGesture(dragGesture, including: .gesture) //Button already has a native tap gesture
                    }
                }
            }
        }
    }
    
    #Preview {
        DragTapTestView()
    }
    

    Note that there are two methods for flipping right – one using a Button, one without.

    More importantly, pay attention to the difference between the left and right methods as you test. Try dragging on the left area multiple times and you may notice that sometimes it can trigger both a drag and a tap at the same time. This may result in a double page flip, depending on your implementation.

    Compare the dragging on the left area with the dragging on any of the right areas, which should not trigger both a drag and a tap.

    Login or Signup to reply.
  2. If the drag gesture doesn’t have to be attached to the blue rectangle in the background, then it works to move the drag gesture to the HStack containing the buttons. Also, if you want the middle button to react to tap gestures too, you can add a .contentShape modifier to it.

    In the updated example below, all buttons (including the clear middle button) react to taps and the whole area is sensitive to drag gestures. The rectangles that were filled with solid color have been replaced with just a plain color (because it’s simpler).

    ZStack {
        Color.blue //simulates spriteKit view
            .ignoresSafeArea()
    
        HStack(spacing: 0) { //my buttons
            Rectangle()
                .fill(.red.opacity(0.00001))
                .border(.red, width: 2)
                .onTapGesture {
                    print("flip page left")
                }
    
            Color.clear
                .contentShape(Rectangle()) // 👈 added
                .border(.green, width: 2)
                .onTapGesture {
                    print("flip page right")
                }
    
            Button {
                print("button PRESSED")
            } label: {
                Color.black
                    .border(.yellow, width: 2)
            }
        }
        .gesture( // 👈 moved to button container
            DragGesture()
                .onChanged { value in
                    //simulates spriteKit drag handling,
                    //for example animated page flipping
                    print("DragGesture",value.velocity)
                }
        )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search