Skip to content

fix(ios): align ShapeAnimator TurboModules with new-arch codegen - #4269

Open
devguy22 wants to merge 1 commit into
rnmapbox:mainfrom
devguy22:fix/shape-animator-new-arch
Open

fix(ios): align ShapeAnimator TurboModules with new-arch codegen#4269
devguy22 wants to merge 1 commit into
rnmapbox:mainfrom
devguy22:fix/shape-animator-new-arch

Conversation

@devguy22

@devguy22 devguy22 commented Jul 31, 2026

Copy link
Copy Markdown

Description

Fixes ShapeAnimator TurboModules on iOS New Architecture so MovePointShapeAnimator / ChangeLineOffsetsShapeAnimator actually bind and animate.

I have this working for my app using an npm patch package, but I'd like to see this upstreamed. Note that this code worked just fine for me before the new Arch on iOS. It also works just fine on Android with both arch.

Root causes:

  • ObjC TurboModule methods did not match NativeRNMBX*ShapeAnimatorModuleSpec (names/types), and getTurboModule returned the wrong SpecJSI class — so JS calls never hit the animator modules.
  • Coordinate arrays often arrive as NSNumber, and as? Double failed, so moveTo rejected valid coordinates.
  • Animator registration hopped async and raced ShapeSource tag lookup, leaving sources unbound.

Changes:

  • Align ObjC method signatures/SpecJSI types with codegen (NSInteger/double, correct method names).
  • Harden coordinate/__nativeTag bridging (NSNumber + Double).
  • Keep animator registration synchronous and lock the manager map so ShapeSource binding is race-free.
  • Ensure refresh() runs on the main thread.

Verified in /example on iOS New Architecture (Mapbox Maps v11) with before/after videos.

Checklist

  • I've read CONTRIBUTING.md
  • I updated the doc/other generated code with running yarn generate in the root folder
  • I have tested the new feature on /example app.
    • In V11 mode/ios
    • In New Architecture mode/ios
    • In V11 mode/android
    • In New Architecture mode/android
  • I added/updated a sample - if a new feature was implemented (/example)

Screenshot OR Video

Before: Shape animators fail to create/bind or do not animate on New Arch iOS.

Simulator.Screen.Recording.-.iPhone.16e.-.2026-07-31.at.15.15.13.mov

After: Animations/AnimatedPoint and Animations/AnimatedLineOffsets create animators, bind to ShapeSource, and animate as expected.

fixed_1.mov

Component to reproduce the issue you're fixing

Use the existing example scenes (no new sample committed):

  1. Just trying to load this will fail (before)
  2. Press 'Start' and then you can also "Remount animator" (after: works)
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Text, View } from 'react-native';
import {
  Camera,
  CircleLayer,
  Logger,
  MapView,
  ShapeSource,
  __experimental,
} from '@rnmapbox/maps';
import type { Position } from 'geojson';

Logger.setLogLevel('verbose');

// create MovePointShapeAnimator once, bind to
// ShapeSource, then drive moveTo on a telemetry-like cadence.
const ORIGIN: Position = [-83.53808787278204, 41.66430343748789];

const BugReportExample = () => {
  const [running, setRunning] = useState(false);
  const [remountKey, setRemountKey] = useState(0);
  const [moveCalls, setMoveCalls] = useState(0);
  const [remounts, setRemounts] = useState(0);

  const positionRef = useRef<Position>([...ORIGIN]);
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

  // Recreate on remount to stress ShapeAnimatorManager register ↔ ShapeSource lookup.
  const animator = useMemo(() => {
    return new __experimental.MovePointShapeAnimator(positionRef.current);
  }, [remountKey]);

  const stop = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
    setRunning(false);
  }, []);

  const tick = useCallback(() => {
    const next: Position = [
      ORIGIN[0]! + (Math.random() - 0.5) * 0.004,
      ORIGIN[1]! + (Math.random() - 0.5) * 0.004,
    ];
    positionRef.current = next;

    animator.moveTo({ coordinate: next, durationMs: 1100 });
    setMoveCalls((n) => n + 1);
  }, [animator]);

  const start = useCallback(() => {
    stop();
    setRunning(true);
    tick();
    timerRef.current = setInterval(tick, 1000);
  }, [stop, tick]);

  const stressRemount = useCallback(() => {
    setRemountKey((k) => k + 1);
    setRemounts((n) => n + 1);
  }, []);

  useEffect(() => () => stop(), [stop]);

  useEffect(() => {
    if (!running) {
      return;
    }
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
    timerRef.current = setInterval(tick, 1000);
  }, [animator, running, tick]);

  return (
    <View style={{ flex: 1 }}>
      <View style={{ padding: 8, gap: 6, backgroundColor: 'white' }}>
        <Text>
          running={String(running)} moves={moveCalls} remounts={remounts}
        </Text>
        <View style={{ flexDirection: 'row', justifyContent: 'space-around' }}>
          <Button
            title={running ? 'Stop' : 'Start'}
            onPress={running ? stop : start}
          />
          <Button title="Remount animator" onPress={stressRemount} />
        </View>
        <Text style={{ fontSize: 12, color: '#444' }}>
          Expected WITH fix: blue circle appears and smoothly chases random
          positions. WITHOUT fix (new arch): compile/link failure, crash, or
          circle missing/stuck while move count still increments.
        </Text>
      </View>

      <MapView style={{ flex: 1 }} key={`map-${remountKey}`}>
        <Camera
          defaultSettings={{ centerCoordinate: ORIGIN, zoomLevel: 14 }}
          centerCoordinate={ORIGIN}
          zoomLevel={14}
        />
        <ShapeSource id="vehicle-shape" shape={animator}>
          <CircleLayer
            id="vehicle-circle"
            style={{ circleColor: 'blue', circleRadius: 10 }}
          />
        </ShapeSource>
      </MapView>
    </View>
  );
};

export default BugReportExample;

Match ObjC method signatures and SpecJSI types to NativeRNMBX*ShapeAnimatorModuleSpec, harden coordinate/tag bridging, and lock animator registration to avoid races with ShapeSource binding.
@devguy22
devguy22 requested a deployment to CI with Mapbox Tokens July 31, 2026 19:47 — with GitHub Actions Waiting
@devguy22
devguy22 requested a deployment to CI with Mapbox Tokens July 31, 2026 19:47 — with GitHub Actions Waiting
@devguy22
devguy22 requested a deployment to CI with Mapbox Tokens July 31, 2026 19:47 — with GitHub Actions Waiting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant