Search code examples
iosswiftuiswiftui-gesture

Tap Gesture on Subview disables drag gesture on super view


I have the following two views in SwiftUI. The first view GestureTestView has a drag gesture defined on its overlay view (call it indicator view) and has the subview called ContentTestView that has tap gesture attached to it. The problem is tap gesture in ContentTestView is blocking Drag Gesture on indicator view. I have tried everything including simultaneous gestures but it doesn't seem to work as gestures are on different views. It's easy to test by simply copying and pasting the code and running the code in XCode preview.

import SwiftUI

struct GestureTestView: View {

   @State var indicatorOffset:CGFloat = 10.0

   var body: some View {
     
      ContentTestView()
         .overlay(alignment: .leading, content: {
        
             Capsule()
               .fill(Color.mint.gradient)
               .frame(width: 8, height: 60)
               .offset(x: indicatorOffset )
               .gesture(
                  DragGesture(minimumDistance: 0)
                    .onChanged({ value in
                          indicatorOffset = min(max(0, 10 + value.translation.width), 340)

                    })
                    .onEnded { value in
                       
                    }
            )
       })
  }
}

 #Preview {
    GestureTestView()
 }

struct ContentTestView: View {

   @State var isSelected = false

   var body: some View {
       HStack(spacing:0) {
         ForEach(0..<8) { index in
              Rectangle()
                .fill(index % 2 == 0 ? Color.blue : Color.red)
                .frame(width:40, height:40)
          }
         .overlay {
             if isSelected {
                 RoundedRectangle(cornerRadius: 5)
                    .stroke(.yellow, lineWidth: 3.0)
            }
          }
    }
     .onTapGesture {
         isSelected.toggle()
     }
    }
 }

#Preview {
   ContentTestView()
}

Solution

  • Changing the order of the modifiers fixes this. onTapGesture should come after overlay.

    Here I have inlined ContentTestView into GestureTestView, and swapped the positions of overlay and onTapGesture.

    struct GestureTestView: View {
        @State var indicatorOffset:CGFloat = 10.0
        @State var isSelected = false
        
        var body: some View {
            HStack(spacing:0) {
                ForEach(0..<8) { index in
                    Rectangle()
                        .fill(index % 2 == 0 ? Color.blue : Color.red)
                        .frame(width:40, height:40)
                }
                .overlay {
                    if isSelected {
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(.yellow, lineWidth: 3.0)
                    }
                }
            }
            .overlay(alignment: .leading, content: {
                Capsule()
                    .fill(Color.mint.gradient)
                    .frame(width: 8, height: 60)
                    .offset(x: indicatorOffset )
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged({ value in
                                indicatorOffset = min(max(0, 10 + value.translation.width), 340)
                                
                            })
                            .onEnded { value in
                                
                            }
                    )
            })
            .onTapGesture {
                isSelected.toggle()
            }
        }
    }
    

    Of course, it is better to break views up into smaller views. In this case, ContentTestView can take its overlay as a view builder closure.

    struct ContentTestView<Overlay: View>: View {
        
        @State var isSelected = false
        
        let alignment: Alignment
        @ViewBuilder let overlay: () -> Overlay
        
        var body: some View {
            HStack(spacing:0) {
                ForEach(0..<8) { index in
                    Rectangle()
                        .fill(index % 2 == 0 ? Color.blue : Color.red)
                        .frame(width:40, height:40)
                }
                .overlay {
                    if isSelected {
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(.yellow, lineWidth: 3.0)
                    }
                }
            }
            .overlay(alignment: alignment, content: overlay)
            .onTapGesture {
                isSelected.toggle()
            }
        }
    }
    
    struct GestureTestView: View {
        @State var indicatorOffset:CGFloat = 10.0
        
        var body: some View {
            ContentTestView(alignment: .leading) {
                Capsule()
                    .fill(Color.mint.gradient)
                    .frame(width: 8, height: 60)
                    .offset(x: indicatorOffset )
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged({ value in
                                indicatorOffset = min(max(0, 10 + value.translation.width), 340)
                                
                            })
                            .onEnded { value in
                                
                            }
                    )
            }
        }
    }