Skip to content

[Bug]: iOS: Camera prop change while MapView is detached from the window is never applied (animated modes) #4304

Closed

Description

@Alvi24

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):

  1. Show a map with <Camera centerCoordinate={A} />.
  2. Deactivate its screen (<Screen activityState={0}> inside a ScreenContainer), which is how a navigator detaches an inactive screen.
  3. Change the prop to B while detached.
  4. 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 A although the prop is B.
  • animationMode="moveTo": 2 of 2 runs, the camera is at B.

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 at B.
  • 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.mm only assigns stop when the prop value differs from the previous one, so the update is sent to the native camera once.
  • RNMBXCamera.stop's didSet calls _updateCamera(). toUpdateItem defaults the mode to .flight, which runs map.mapView.camera.fly(to:duration:).
  • In Mapbox Maps iOS, MapView.didMoveToWindow sets cameraAnimatorsRunner.isEnabled = false when window == nil. The doc comment on CameraAnimatorsRunner.isEnabled says that while it is false "any new animations will be canceled immediately".
  • I found nothing in RNMBXMapView that re-applies the stop when the view gets a window again. initialLayout() runs once, behind isPendingInitialLayout.
  • .move / .none go through map.mapboxMap.setCamera(to:) with no animator, which fits moveTo being 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-screens 4.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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions