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 then drag 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 view
package/ios/HybridMapViewDelegate.swift:55-62 — handleLongPress(_:), no annotation check
package/ios/HybridMapViewDelegate.swift:64-69 — shouldRecognizeSimultaneouslyWith → true
package/ios/HybridMapViewDelegate.swift:37-39, :71-80 — the guard handleTap has, and
the isAnnotationView(at:in:) helper it uses
package/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 fact
package/ios/AppleMapProviderAdapter.swift:375-378 — notifyLongPress(at:)
package/ios/NitroPinAnnotationView.swift:28, package/ios/NitroImageAnnotationView.swift:24
— isDraggable = marker.draggable
docs/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
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, or nil → delivered
CI does not run this target today (#120), so the test needs a local run.
Related
Problem
On the Apple provider, starting a marker drag also fires the map's
onLongPress.HybridMapViewDelegateinstalls aUILongPressGestureRecognizeron the wholeMKMapView(
HybridMapViewDelegate.swift:18-24) and lets it recognise alongside every other recognizer(
shouldRecognizeSimultaneouslyWithreturnstrue,:64-69). MapKit starts a drag of adraggable 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
.beganand callsnotifyLongPress(at:)(:55-62) without checking what is underthe finger:
The tap path next to it already makes that check —
handleTap(_:)returns beforenotifyPresswhenisAnnotationView(at:in:)is true (:37-39) — and the docs promise thesame for both events:
onPress/onLongPressare "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 adrag or a map long press. The Android reference documents
onMapLongClickas called "only ifnone 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
onLongPressis "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:
onLongPressfires once per drag, at the moment the drag begins. The code has been this waysince the delegate's first commit (601f796).
Reproduction
Press and hold the marker until it lifts, drag it anywhere, release.
map long press {…}and thendrag end museum {…}.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 showsLong · 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 thewhole map view
package/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, andthe
isAnnotationView(at:in:)helper it usespackage/ios/HybridMapViewDelegate.swift:224-243— the drag-state delegate; only.endingis forwarded, so nothing on the drag side can suppress the long press after the fact
package/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" contractSuggested 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
UIGestureRecognizerDelegateof both recognizers, so this is one more delegate method:The minimal alternative is the guard
handleTapuses, placed insidehandleLongPress: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
isAnnotationViewstays the single helper, so the tap and long-press paths remainsymmetric. 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
MKAnnotationViewas "not background": draggable and non-draggablemarkers, cluster badges (
NitroClusterAnnotationView) and the user-location view alike. Thatis exactly what
handleTapdoes today and what the docs promise, so a long press on anon-draggable marker fires nothing. If a marker long-press event is wanted later it should be
its own callback (
onMarkerLongPress), notonLongPresswith 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
onMarkerDragEnd(and the JSXmarker's
onDragEnd) and neveronLongPressonLongPresswith the pressed coordinateno map
onLongPress(same as tap today)handleTapbehaviour is unchangedmarker
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 —MKAnnotationViewand
UIVieware constructible in a test bundle:MKAnnotationView→ long press not deliveredMKAnnotationView→ not deliveredUIView, ornil→ deliveredCI does not run this target today (#120), so the test needs a local run.
Related