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()
}
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
}
)
}
}
}