iOS / Apple Maps: custom Marker is mispositioned when its content changes size (New Architecture)
Summary
On Apple Maps (PROVIDER_DEFAULT) with the New Architecture, a custom <Marker>
whose child changes size ends up mispositioned — it stays placed as though it
still had a previous, larger size. Markers whose child never changes size are
placed correctly, which is why this is easy to miss.
The cause is that AIRMapMarker's bounds are derived from the first subview and
only ever grow:
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect reactFrame = self.frame;
UIView *firstSubView = self.subviews.firstObject;
if (firstSubView && (CGRectGetWidth(firstSubView.frame) > CGRectGetWidth(reactFrame) ||
CGRectGetHeight(firstSubView.frame) > CGRectGetHeight(reactFrame))) {
reactFrame = firstSubView.frame; // <- taken ONLY when larger
}
[self reactSetFrame:reactFrame];
}reactSetFrame: publishes that size as self.bounds, and MapKit centres an
annotation view on its bounds. So once a marker has been wide, it keeps the wide
bounds, and its visible content is drawn off-centre by half the difference —
permanently, since the size never comes back down.
Environment
| react-native-maps | 1.27.2 |
| react-native | 0.85.3 (New Architecture / Fabric) |
| Expo SDK | 56 |
| Platform | iOS Simulator, PROVIDER_DEFAULT (Apple Maps) |
Google Maps on iOS and Android are not affected — they honour the fractional
anchor prop, which Apple Maps ignores.
Reproduction
Render a <Marker> whose child's width depends on state, and change that state:
<Marker coordinate={coord} tracksViewChanges={tracks}>
<View style={{ backgroundColor: '#000', paddingHorizontal: 10, height: 28 }}>
<Text style={{ color: '#fff' }}>{label}</Text>
</View>
</Marker>Switch label from a long string to a short one. The marker keeps the wider
bounds and the visible pill sits off to one side of the coordinate. Switching
back to a long label appears to "fix" it, which makes it read like an
intermittent bug.
Measurements
Two labels of the same design, on the same coordinate:
- content ≈148pt wide → coordinate fell ≈66pt from the content's left edge
- content ≈272pt wide → coordinate fell ≈64pt from the content's left edge
A near-constant absolute offset across very different content widths — the signature of bounds that are inherited rather than measured.
Two fixes that do NOT work (both tested on device)
1. Deleting -layoutSubviews and -setFrame: — i.e. matching what
v2.0.0-beta.15 already does — results in no marker rendering at all on the
New Architecture.
The reason matters for 2.x: RNMapsMarkerView.mm (the Fabric component view) has
no updateLayoutMetrics, no layoutSubviews and no setFrame override, and
never assigns _view.frame; it only mounts children onto the legacy view via
insertReactSubview:. So under Fabric, -layoutSubviews is the only code that
ever sizes the marker. v2.0.0-beta.15 can delete it because its marker is
Paper-only (ios/Maps/ contains RNMMapMarker + manager and no Fabric component
view), where the UIManager calls reactSetFrame: directly.
2. Relaxing the condition to take the subview's frame unconditionally makes
the marker drift upward off the screen — reactSetFrame: then runs on every
layout pass, and each pass with a non-zero dy subtracts from self.center.y.
So the fix is not simply "delete it" or "always take it"; the interaction between
-layoutSubviews, reactSetFrame:'s center rewrite, and Fabric's lack of a
layout override needs to be resolved together. I did not want to guess at that in
a PR without maintainer input on the intended ownership of layout.
Workaround for users
Give the marker's child a constant size, and vary only what is drawn inside
it. A fixed-size <Svg> works well, because it is a single native view whose
dimensions never change:
// canvas is CONSTANT for every label; only the pill drawn inside it varies
<Marker coordinate={coord} tracksViewChanges={tracks}>
<Svg width={456} height={72} viewBox="0 0 456 72">
<Rect x={236} y={22} width={pillWidth} height={28} rx={14} fill="rgba(0,0,0,0.72)" />
<SvgText x={248} y={36} fontSize={14} fill="#fff" alignmentBaseline="central">
{label}
</SvgText>
</Svg>
</Marker>Because the canvas never changes size, reactSetFrame: computes dy = 0, bounds
never ratchet, and MapKit's centring is stable. Any offset you need (e.g. placing
the content beside the coordinate rather than on it) can be baked into where you
draw inside the canvas — no anchor or centerOffset required, which is
convenient given anchor is a no-op on Apple Maps.
This is what the library's own MKMarkerAnnotationView-style pins do implicitly:
fixed-size children have never exhibited the problem.
Note for 2.x
reactSetFrame:'s center rewrite is byte-identical in 1.27.2, 1.28.0, 1.29.0
and 2.0.0-beta.15. The two methods above are gone in 2.0, which likely resolves
the ratchet for the Paper marker — but if a Fabric marker component lands in 2.x,
whatever sizes the annotation will need to be re-established, or the same
"no marker at all" result will follow. I have not been able to test 2.0 directly.
Happy to open a PR if maintainers can say where layout should be owned under Fabric — the fix looks small once that is decided.
Source: react-native-maps/react-native-maps