MapProxy conversion from screen to coords is wrong on macOS

Try the following code on macOS, and you'll see the marker is added in the wrong place, as the conversion from screen coordinates to map coordinates doesn't work correctly.

The screenCoord value is correct, but reader.convert(screenCoord, from: .local) offsets the resulting coordinate by the height of the content above the map, despite the .local parameter.

struct TestMapView: View {
    @State var placeAPin = false
    @State var pinLocation :CLLocationCoordinate2D? = nil
    
    @State private var cameraProsition: MapCameraPosition = .camera(
        MapCamera(
            centerCoordinate: .denver,
            distance: 3729,
            heading: 92,
            pitch: 70
        )
    )
    
    var body: some View {
        VStack {
            Text("This is a bug demo.")
            Text("If there are other views above the map, the MapProxy doesn't convert the coordinates correctly.")
            
            MapReader { reader in
                Map(
                    position: $cameraProsition,
                    interactionModes: .all
                )
                {
                    if let pl = pinLocation {
                        Marker("(\(pl.latitude), \(pl.longitude))", coordinate: pl)
                    }
                }
                .onTapGesture(perform: { screenCoord in
                    pinLocation = reader.convert(screenCoord, from: .local)
                    placeAPin = false
                    
                    if let pinLocation {
                        print("tap: screen \(screenCoord), location \(pinLocation)")
                    }
                })
                
                .mapControls{
                    MapCompass()
                    MapScaleView()
                    MapPitchToggle()
                }
                
                .mapStyle(.standard(elevation: .automatic))
            }
            
        }
    }
}

extension CLLocationCoordinate2D {
    static var denver = CLLocationCoordinate2D(latitude: 39.742043, longitude: -104.991531)
}

(FB13135770)

What is screenCoord? Where does it come from?

It's a point. The closure argument for .onTapGesture(perform:).

(Oops, this was supposed to be a comment; I don't see how to delete it.)

Can you try using a custom coordinate space like this and see if it works. I can't test it because I'm on Ventura.

Map {
    ...    
}
.onTapGesture(perform: { screenCoord in
    pinLocation = reader.convert(screenCoord, from: .named("map"))
    ...
})
.coordinateSpace(.named("map"))

Just ran into this. But... I don't have any views above the map. In my case the Y coordinate seems to be off by 25 points. If I add 25.0 to the Y coordinate of the reported position before conversion I get what I believe to be the correct value. Or certainly something much, much closer to the correct value.

macOS 14.3.1.

I have tried:

import MapKit
import SwiftUI

private let rectWidth: Double = 80

private struct MarkerData {
    let coordinate: CLLocationCoordinate2D
    let screenPoint: CGPoint

    var touchableRect: CGRect {
        .init(x: screenPoint.x - rectWidth / 2, y: screenPoint.y - rectWidth / 2, width: rectWidth, height: rectWidth)
    }
}

struct ContentView: View {

    @State private var cameraPosition: MapCameraPosition = .automatic
    @State private var modes: MapInteractionModes = [.all]
    @State private var isMarkerDragging = false
    @State private var markerData: MarkerData?

    var body: some View {
        GeometryReader { geometryProxy in
            MapReader { mapProxy in
                Map(position: $cameraPosition, interactionModes: modes) {
                    if let markerData {
                        Marker("Start", coordinate: markerData.coordinate)
                    }
                }
                .onTapGesture { screenCoordinate in
                    self.markerData = mapProxy.markerData(screenCoordinate: screenCoordinate, geometryProxy: geometryProxy)
                }
                .highPriorityGesture(DragGesture(minimumDistance: 1)
                    .onChanged { drag in
                        guard let markerData else { return }
                        if isMarkerDragging {

                        } else if markerData.touchableRect.contains(drag.startLocation) {
                            isMarkerDragging = true
                            setMapInteraction(enabled: false)
                        } else {
                            return
                        }

                        self.markerData = mapProxy.markerData(screenCoordinate: drag.location, geometryProxy: geometryProxy)
                    }
                    .onEnded { drag in
                        setMapInteraction(enabled: true)
                        isMarkerDragging = false
                    }
                )
                .onMapCameraChange {
                    guard let markerData else { return }
                    self.markerData = mapProxy.markerData(coordinate: markerData.coordinate, geometryProxy: geometryProxy)
                }
            }
        }
    }

    private func setMapInteraction(enabled: Bool) {
        if enabled {
            modes = .all
        } else {
            modes = []
        }
    }
}

private extension MapProxy {

    func markerData(screenCoordinate: CGPoint, geometryProxy: GeometryProxy) -> MarkerData? {
        guard let coordinate = convert(screenCoordinate, from: .local) else { return nil }
        return .init(coordinate: coordinate, screenPoint: screenCoordinate)
    }

    func markerData(coordinate: CLLocationCoordinate2D, geometryProxy: GeometryProxy) -> MarkerData? {
        guard let point = convert(coordinate, to: .local) else { return nil }
        return .init(coordinate: coordinate, screenPoint: point)
    }
}

by user nightwill from this question at StackOverflow:

https://stackoverflow.com/questions/77354568/swiftui-mapkit-drag-annotation

and I noticed that on macOS 14.3.1 and Xcode 15 the touchable rect is under the marker most of the time, except when the only window in the app is the map and the app is in full screen.

You can check the touchable rect by implementing this in the code from nightwill (I implemented it after onMapCameraChange in the code above):

.overlay {
if let markerData {
    Rectangle()
        .stroke(Color.red, lineWidth: 2)
        .frame(width: rectWidth, height: rectWidth)
        .position(x: markerData.screenPoint.x, y: markerData.screenPoint.y)
    }
}
MapProxy conversion from screen to coords is wrong on macOS
 
 
Q