Reputation: 541
I have a SwiftUI Map with MapAnnotations. I would like to have an onTap gesture on the Map, so it deselects the selected annotations, and dissmisses a bottom sheet, etc. Also would like to have an onTap gesture on the annotation item (or just having a button as annotation view with an action there), which selects the annotation and do stuff. The problem: whenever I tap the annotation, the map's ontap gesture is triggered too. (When I tap on the map, it only triggers the map's action, so no problems there.) Here's some sample code:
import SwiftUI
import MapKit
import CoreLocation
struct ContentView: View {
@State var region: MKCoordinateRegion =
MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 47.333,
longitude: 19.222),
span: MKCoordinateSpan(latitudeDelta: 0.002, longitudeDelta: 0.002))
var body: some View {
Map(coordinateRegion: $region,
annotationItems: AnnotationItem.sample) { annotation in
MapAnnotation(coordinate: annotation.location.coordinate) {
VStack {
Circle()
.foregroundColor(.red)
.frame(width: 50)
Text(annotation.name)
}
.onTapGesture {
print(">> tapped child")
}
}
}
.onTapGesture {
print(">> tapped parent")
}
}
}
I tap on the annotation, then:
>> tapped parent
>> tapped child
I tap on the map, then:
>> tapped parent
EDIT:
I have tried and didn't work:
Upvotes: 7
Views: 2862
Reputation: 29
I'm seeing a lot of replies on here that just seem suboptimal. Using a different gesture that mimics a tap gesture is kind of a hack and may not continue working in the future. I came across this thread but eventually found a different way that just makes a lot more sense. Essentially, the problem is there are overlapping tap gestures, and which one will capture the event is not determinant. Instead, the map annotation should be given a higher priority, since we can always assume a tap on an annotation will always take precedence over a tap on the map itself. To do so, we simply need to do the following
.highPriorityGesture(
TapGesture()
.onEnded {
// perform logic
}
)
and the annotation will now always capture the tap instead of the map sometimes taking it. Not only does this avoid a hacky workaround, it also clearly indicates in the code the expected behaviour.
Upvotes: 1
Reputation: 251
Another little hack inspired by @Gergely Kovacs's above answer!
var body: some View {
Map(coordinateRegion: $region, interactionModes: [.zoom, .pan], annotationItems: AnnotationItem.sample) { annotation in
MapAnnotation(coordinate: annotation.location.coordinate) {
VStack {
Circle()
.foregroundColor(.red)
.frame(width: 50)
Text(annotation.name)
}
.onLongPressGesture(minimumDuration: .zero, maximumDistance: .zero) {
print(">> Tapped child")
}
}
}
.onTapGesture {
print(">> tapped parent")
}
}
onLongPressGesture
has higher priority than onTapGesture
Upvotes: 5
Reputation: 15217
Warning: The workaround shown in the edit below works apparently only in special cases, see the comment of Gergely Kovacs below.
This seems to me to be a bug, since the default behavior is that only one gesture recognizer fires at a time, see here.
A similar problem occurs in a ScrollView
, but there exists a property .delaysContentTouches
to solve it, see here. This does unfortunately not exist for a View
.
A possible workaround is to delay the parent tap action until it is ensured that no child tap action follows. You could add to your ContentView
a @State var childTapTriggered = false
and set this var
to true
if it triggered. Then you could use as parent tap gesture closure something like
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if !childTapTriggered {
// do parent action
}
}
EDIT (due to the comment of Gergely Kovacs):
The above workaround does not work, sorry, pls see the comment.
But I tested the following workaround, and it works in my case:
I added to the ContentView a state var:
@State var childTapped = false
On the annotation view (the child), I have the following modifier:
.onTapGesture {
print(">> tapped child")
childTapped = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
childTapped = false
}
}
On the map (the parent) I have the following modifier:
.onTapGesture {
if !childTapped {
print(">> tapped parent")
}
}
Of course this is again a hack, and the delay to reset childTapped
to false
had to be adjusted right.
Anyway, maybe this solves your problem!
Upvotes: 0
Reputation: 541
Currently one little hack seems to work! I change the onTap gesture to a DragGesture with a minimum distance of 0.
var body: some View {
Map(coordinateRegion: $region, interactionModes: [.zoom, .pan], annotationItems: AnnotationItem.sample) { annotation in
MapAnnotation(coordinate: annotation.location.coordinate) {
VStack {
Circle()
.foregroundColor(.red)
.frame(width: 50)
Text(annotation.name)
}.gesture(childTapGesture)
}
}
.onTapGesture {
print(">> tapped parent")
}
}
let childTapGesture = DragGesture(minimumDistance: 0).onEnded {_ in
print(">> Tapped child")
}
}
and it works! The problem with this solution, is that touching one of the pins while dragging the map, triggers the pin action unintentionally. Thus my answer will not be the accepted one
Upvotes: 1