iosswiftuigesturegeometryreader

Why do my SwiftUI gestures on a Map work only once?


Setup:

I am converting an app from UIKit to SwiftUI.
The app uses now instead of an MKMapView a SwiftUI Map.
Unfortunately, not all features of an MKMapView are already provided by Apple for Map, e.g. dragging a map annotation and getting the coordinate of the drag end point.
Anyway, I am trying thus to implement a similar function in SwiftUI. This is the development body of my annotation view:

var body: some View {
    GeometryReader { geo in // 1
        let frameInGlobal = geo.frame(in: .global)
        Image("PinRed")
            .background(Color.green) // 2
            .offset(CGSize(width: dragAmount.width, height: dragAmount.height - liftOff)) // 3
            .defersSystemGestures(on: .all) // 4
            .onTapGesture {
                print("Global center: \(frameInGlobal.midX) x \(frameInGlobal.midY)")
            }
            .gesture(drag(frameInGlobal: frameInGlobal)) // 5
            .simultaneousGesture( // 6
                LongPressGesture(minimumDuration: 0.2)
                    .onEnded { _ in // 7
                        liftOff = 40.0
                }
            )
    }
    .frame(width: 30, height: 30)
    .background(Color.red) // 2
}

<1> GeometryReader is used to get the screen coordinates during dragging.
<2> The background colors are there so that it can easily be identified what happens.
<3> The pin image is offset by the drag amount plus a vertical shift so that the pin is visible above the finger.
<4> The system gestures, e.g. zoom in after double tap and long press to drag the map, are deferred so that the custom gestures come through.
<5> The custom drag gesture essentially changes a @GestureState var dragAmount = CGSize.zero so that the pin image follows the finger.
<6> A simultaneousGesture is required so that the custom tap gesture as well as the custom long press gesture can be used.
<7> That the pin is lift off, indicates that dragging can start.

Problem:

If the liftOff value is 0.0, I can tap the annotation, which prints out its position, and I can long press it and drag it around. After dragging stopped, it still accepts further taps and long presses.
However, when the liftOff value is e.g. 40.0, the annotation can only be tapped as long as it is not dragged, and when it has been dragged the 1st time, it cannot be dragged again. This means the custom gestures do no longer come through. Instead, only the system gestures work.
Here is an example of the situation after the 1st drag (green is the image background, red the GeometryReader background).

enter image description here

Question:

Why are the custom gestures only accepted when the pin is not lift off?
And how to do it right?

Edit:

I just found out that my custom gestures work as long as Image (green square) and GeometryReader (red square) overlap, and the tap point, long press point and start point of a drag are within the overlap area.


Solution

  • Problem solved. It has nothing to do with SwiftUI:
    If in the code example above liftOff is 0, the green View (Image) and the red View (GeometryReader) have always the same position on the screen. If the green View is tapped, the hit position is forwarded to the custom gestures. Since the red View has the same position on the screen, the red View handles the hit.
    If however liftOff is larger than the frame size of the green and red View, both do not overlap on the screen. If now the green View is tapped, the hit position is forwarded to the red View that does not overlap the green View, i.e. the red View is not hit, and the hit is forwarded to the next View behind the green View, which is the Map itself. Thus the custom gestures are not triggered, but the system gestures.

    In the code above, this can easily be checked by adding line 3 additionally as last view modifier of the GeometryReader.