Repository navigation
Apple Maps marker drag also fires the map's onLongPress #132
Description
Problem
On the Apple provider, starting a marker drag also fires the map's onLongPress.
HybridMapViewDelegate installs a UILongPressGestureRecognizer on the whole MKMapView
(HybridMapViewDelegate.swift:18-24) and lets it recognise alongside every other recognizer
(shouldRecognizeSimultaneouslyWith returns true, :64-69). MapKit starts a drag of a
draggable annotation view from a press-and-hold on that view. So one press-and-hold on a
draggable marker is recognised twice: MapKit begins the drag, and the delegate's recognizer
reaches .began and calls notifyLongPress(at:) (:55-62) without checking what is under
the finger:
@objc private func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
guard recognizer.state == .began, let parent else {
return
}
let point = recognizer.location(in: parent.view)
parent.notifyLongPress(at: point) // <- fires on a marker too
}The tap path next to it already makes that check — handleTap(_:) returns before
notifyPress when isAnnotationView(at:in:) is true (:37-39) — and the docs promise the
same for both events: onPress / onLongPress are "Map background only"
(docs/architecture.md:71). Long press is the one place the promise is not kept.
Neither Google provider has this problem, by construction: they own no recognizer and only
implement the SDK delegate callbacks (GoogleMapProviderAdapter.swift:548-558,
GoogleMapProviderAdapter.kt:457-490), so the SDK decides whether a press-and-hold is a
drag or a map long press. The Android reference documents onMapLongClick as called "only if
none of the overlays of the map handled the gesture", and a drag start is such a handling.
The practical effect: the most common use of onLongPress is "long-press to add a marker",
and such an app drops a phantom marker under every marker the user starts to drag.
Evidence
Audit run of 2026-09-17 against v1.2.1, fresh Expo SDK 57 consumer app, iOS 26.5 simulator,
Apple provider. One press-and-hold-and-drag on a draggable JSX marker logged, in order:
jsx.onLongPress {…21.0299}
jsx.marker.b.onDragEnd …
onLongPress fires once per drag, at the moment the drag begins. The code has been this way
since the delegate's first commit (601f796).
Reproduction
<MapView
style={{ flex: 1 }}
region={{ latitude: 52.2322, longitude: 20.981, latitudeDelta: 0.05, longitudeDelta: 0.05 }}
onLongPress={(c) => console.log('map long press', c)}
onMarkerDragEnd={(id, c) => console.log('drag end', id, c)}
>
<Marker id="museum" coordinate={{ latitude: 52.2322, longitude: 20.981 }} draggable />
</MapView>Press and hold the marker until it lifts, drag it anywhere, release.
- Apple:
map long press {…}and thendrag end museum {…}. - Expected (and what the Google providers do):
drag end museum {…}only.
In the example app: Landmarks scenario (example/examples/landmarks.ts:44, the
"Warsaw Uprising Museum" pin is draggable). The status line shows Long · 52.23…, 20.98…
the moment the pin lifts, then warsaw-uprising-museum → … on drop.
Where
package/ios/HybridMapViewDelegate.swift:18-24— the long-press recognizer, attached to the
whole map viewpackage/ios/HybridMapViewDelegate.swift:55-62—handleLongPress(_:), no annotation checkpackage/ios/HybridMapViewDelegate.swift:64-69—shouldRecognizeSimultaneouslyWith→truepackage/ios/HybridMapViewDelegate.swift:37-39,:71-80— the guardhandleTaphas, and
theisAnnotationView(at:in:)helper it usespackage/ios/HybridMapViewDelegate.swift:224-243— the drag-state delegate; only.ending
is forwarded, so nothing on the drag side can suppress the long press after the factpackage/ios/AppleMapProviderAdapter.swift:375-378—notifyLongPress(at:)package/ios/NitroPinAnnotationView.swift:28,package/ios/NitroImageAnnotationView.swift:24
—isDraggable = marker.draggabledocs/architecture.md:71— the documented "map background only" contract
Suggested fix
Decide at touch-down, before MapKit does anything, by refusing the touch for the long-press
recognizer when it lands on an annotation view. The delegate is already the
UIGestureRecognizerDelegate of both recognizers, so this is one more delegate method:
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
guard gestureRecognizer is UILongPressGestureRecognizer, let parent else {
return true
}
return !isAnnotationView(at: touch.location(in: parent.view), in: parent.view)
}The minimal alternative is the guard handleTap uses, placed inside handleLongPress:
let point = recognizer.location(in: parent.view)
if isAnnotationView(at: point, in: parent.view) {
return
}
parent.notifyLongPress(at: point)That works as long as the annotation view is still under the finger at .began (0.5 s).
MapKit may have started the drag by then; if it has lifted or offset the view, the hit test
at the finger can miss a small custom image marker and the long press leaks again. The
touch-down check has no such window, which is why it is the recommendation.
Either way isAnnotationView stays the single helper, so the tap and long-press paths remain
symmetric. Per the repo's Swift layout rules the superview walk can move to its own file
(e.g. UIView+isInsideAnnotationView.swift), which also makes it directly testable.
Notes
The guard treats any MKAnnotationView as "not background": draggable and non-draggable
markers, cluster badges (NitroClusterAnnotationView) and the user-location view alike. That
is exactly what handleTap does today and what the docs promise, so a long press on a
non-draggable marker fires nothing. If a marker long-press event is wanted later it should be
its own callback (onMarkerLongPress), not onLongPress with a marker under the finger.
The audit did not execute a marker drag on the Google providers (there is no automation for
it on Android), so their behaviour here rests on the SDK contract, not on a measurement.
Acceptance criteria
- Press-and-hold-and-drag on a draggable marker fires
onMarkerDragEnd(and the JSX
marker'sonDragEnd) and neveronLongPress - A long press on the map background still fires
onLongPresswith the pressed coordinate - A long press on a non-draggable marker, a cluster badge or the user-location view fires
no maponLongPress(same as tap today) -
handleTapbehaviour is unchanged - Verified on the iOS simulator with the Apple provider, with a pin marker and an image
marker
Regression tests
XCTest in package/iosTests/ (podspec test spec, package/react-native-better-maps.podspec:70-72;
the pod links MapKit). With the hit test injected into the delegate (default
mapView.hitTest(_:with:)), the decision is testable without a live map — MKAnnotationView
and UIView are constructible in a test bundle:
- hit view is an
MKAnnotationView→ long press not delivered - hit view is a plain subview nested inside an
MKAnnotationView→ not delivered - hit view is a plain
UIView, ornil→ delivered
CI does not run this target today (#120), so the test needs a local run.
Related
- Apple Maps swallows onPress under a tappable overlay that has no press handler #106, Apple Maps polyline press hit-tests the filled shape instead of the stroke #126 — the tap side of the same delegate; independent fixes
- CI never compiles Swift or Kotlin #120 — CI never compiles Swift or runs the XCTest target
Activity
Aaqibhafeezkhan commentedon Sep 17, 2026
I'd like to work on this issue. I'll implement the touch-down guard for the Apple Maps long-press recognizer, add the requested regression coverage, and verify that background long-press behavior remains unchanged.
jkasprzyk17 commentedon Sep 23, 2026
@Aaqibhafeezkhan
Sounds good, go ahead and take it! Thanks for taking care of this.
test(ios): add simulator coverage for marker drags (gmi-software#132)
test(ios): add simulator coverage for marker drags (gmi-software#132)
fix(ios): suppress map long press on marker drags (gmi-software#132)