Repository navigation
Description
Mapbox Version
default (11.23.1)
React Native Version
0.86.3
Platform
iOS
@rnmapbox/maps version
10.3.5
Standalone component to reproduce
It runs by itself and prints the camera centre on screen, so no interaction is needed. The one extra import is react-native-screens (already a dependency of /example): it is the only way I found to take a mounted MapView out of the window from JS, which is what a tab or stack navigator does to an inactive screen.
import React, { useEffect, useRef, useState } from 'react';
import { Text, View } from 'react-native';
import { Camera, MapView } from '@rnmapbox/maps';
import { Screen, ScreenContainer } from 'react-native-screens';
const A = [-74.00597, 40.71427];
const B = [-73.95, 40.78];
// Each run: show the map at A, detach its screen from the window, change the
// Camera `centerCoordinate` prop to B while detached, re-attach, then read
// where the camera actually is. Every run mounts a fresh MapView.
const RUNS = [
{ name: 'default mode', animationMode: undefined },
{ name: 'default mode', animationMode: undefined },
{ name: 'default mode', animationMode: undefined },
{ name: 'moveTo', animationMode: 'moveTo' },
{ name: 'moveTo', animationMode: 'moveTo' },
];
const format = (center) =>
center ? center.map((value) => value.toFixed(4)).join(', ') : 'n/a';
const BugReportExample = () => {
const map = useRef(null);
const [index, setIndex] = useState(0);
const [attached, setAttached] = useState(true);
const [center, setCenter] = useState(A);
const [log, setLog] = useState([]);
const run = RUNS[index];
useEffect(() => {
if (!run) return undefined;
const report = async (label) => {
const current = await map.current?.getCenter();
setLog((lines) => [
...lines,
`#${index + 1} [${run.name}] ${label} -> ${format(current)}`,
]);
};
const timers = [
setTimeout(() => report('attached, prop=A'), 3000),
setTimeout(() => setAttached(false), 4000),
// the prop changes while the map is detached
setTimeout(() => setCenter(B), 5000),
setTimeout(() => setAttached(true), 7000),
setTimeout(() => report('re-attached, prop=B'), 12000),
// next run, on a fresh map at A
setTimeout(() => {
setCenter(A);
setIndex(index + 1);
}, 13000),
];
return () => {
for (const timer of timers) clearTimeout(timer);
};
}, [index, run]);
return (
<View style={{ flex: 1, paddingTop: 60, backgroundColor: 'white' }}>
<Text style={{ padding: 8, color: 'black', fontSize: 11 }}>
A={format(A)} B={format(B)} |{' '}
{run ? `run ${index + 1}/${RUNS.length}` : 'done'}
{'\n'}
{log.join('\n')}
</Text>
{run && (
<ScreenContainer key={index} style={{ flex: 1 }}>
<Screen activityState={attached ? 2 : 0} style={{ flex: 1 }}>
<MapView ref={map} style={{ flex: 1 }}>
<Camera
centerCoordinate={center}
zoomLevel={12}
animationMode={run.animationMode}
/>
</MapView>
</Screen>
</ScreenContainer>
)}
</View>
);
};
export default BugReportExample;This is the component I ran. The only line removed is my app's access-token import.
Observed behavior and steps to reproduce
When a <Camera> prop changes while the MapView is detached from the window, the camera never moves to the new value on iOS, even after the view is attached again. The prop and the camera then disagree until the prop changes once more.
Steps (the component does them on a timer):
- Show a map with
<Camera centerCoordinate={A} />. - Deactivate its screen (
<Screen activityState={0}>inside aScreenContainer), which is how a navigator detaches an inactive screen. - Change the prop to
Bwhile detached. - Re-attach the screen, wait 5 seconds, read
getCenter().
On-screen output, iOS 27.0 simulator (iPhone 18 Pro, Xcode 27.0):
A=-74.0060, 40.7143 B=-73.9500, 40.7800 | done
#1 [default mode] attached, prop=A -> -74.0058, 40.7142
#1 [default mode] re-attached, prop=B -> -74.0060, 40.7143
#2 [default mode] attached, prop=A -> -74.0059, 40.7142
#2 [default mode] re-attached, prop=B -> -74.0060, 40.7143
#3 [default mode] attached, prop=A -> -74.0060, 40.7143
#3 [default mode] re-attached, prop=B -> -74.0060, 40.7143
#4 [moveTo] attached, prop=A -> -74.0060, 40.7143
#4 [moveTo] re-attached, prop=B -> -73.9500, 40.7800
#5 [moveTo] attached, prop=A -> -74.0060, 40.7143
#5 [moveTo] re-attached, prop=B -> -73.9500, 40.7800
- Default animation mode: 3 of 3 runs, the camera is still at
Aalthough the prop isB. animationMode="moveTo": 2 of 2 runs, the camera is atB.
The same component on an Android 16 emulator with the same @rnmapbox/maps version: 5 of 5 runs end at B, in both modes. So I only see this on iOS.
Two controls from a single run of an earlier, longer variant of the component on the same iOS simulator:
- Hiding the map with
display: 'none'instead of detaching it, and changing the prop while hidden: the camera ends atB. - After a lost update, later prop changes made while the map is visible apply normally.
How I hit it in an app: a tab navigator with the map on one tab. Another tab changes the state that feeds centerCoordinate and then navigates to the map tab; the map does not move.
Expected behavior
After the view is attached again, the camera matches the current <Camera> props, as it does on Android and as it does on iOS with animationMode="moveTo".
If dropping the update is intended, a note in the Camera docs would help, since the prop API reads as declarative.
Notes / preliminary analysis
From reading the 10.3.5 sources and Mapbox Maps iOS 11.23.1. I have not verified this with a patched build, so treat it as a lead, not a diagnosis.
RNMBXCameraComponentView.mmonly assignsstopwhen the prop value differs from the previous one, so the update is sent to the native camera once.RNMBXCamera.stop'sdidSetcalls_updateCamera().toUpdateItemdefaults the mode to.flight, which runsmap.mapView.camera.fly(to:duration:).- In Mapbox Maps iOS,
MapView.didMoveToWindowsetscameraAnimatorsRunner.isEnabled = falsewhenwindow == nil. The doc comment onCameraAnimatorsRunner.isEnabledsays that while it is false "any new animations will be canceled immediately". - I found nothing in
RNMBXMapViewthat re-applies the stop when the view gets a window again.initialLayout()runs once, behindisPendingInitialLayout. .move/.nonego throughmap.mapboxMap.setCamera(to:)with no animator, which fitsmoveTobeing unaffected.
A possible direction, untested: remember that a stop was issued while the map had no window and re-run it when the map view moves back to a window.
Workarounds that work for me: animationMode="moveTo", or calling camera.setCamera(...) from the screen's focus effect instead of relying on the prop.
Additional links and references
- New Architecture enabled, Expo SDK 57,
react-native-screens4.26.2. - Related: [Bug]: changing centerCoordinate prop doesn't move camera when screen is not focused #4203, my earlier report of the same symptom. This one adds a working reproducer, narrows it to animated updates on iOS, and is on current versions.
- The reproduction runs and this write-up were done with an AI coding assistant; the output above is copied from the simulator and emulator screens.
Activity
fix(ios): apply camera updates made while the map is off-window; repl…