Skip to content

Apple Maps marker drag also fires the map's onLongPress #132

Description

@jkasprzyk17

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

  • Press-and-hold-and-drag on a draggable marker fires onMarkerDragEnd (and the JSX
    marker's onDragEnd) and never onLongPress
  • A long press on the map background still fires onLongPress with the pressed coordinate
  • A long press on a non-draggable marker, a cluster badge or the user-location view fires
    no map onLongPress (same as tap today)
  • handleTap behaviour 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, or nil → delivered

CI does not run this target today (#120), so the test needs a local run.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingconfirmedReproduced and accepted by a maintainergood first issueGood for newcomershelp wantedExtra attention is neededplatform: iosAffects iOSprovider: appleAffects the Apple Maps (MapKit) providerswiftThe Swift / iOS native layer (package/ios)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions