Skip to content

[Bug]: iOS: view-backed <Image> icons are silently dropped when the style is still loading (Images does not waitForStyleLoad) #4287

Description

@ben-shokati-sh

Mapbox Version

11.29.0-rc.1

React Native Version

0.86.2

Platform

iOS

@rnmapbox/maps version

10.3.5

Standalone component to reproduce

import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  Image,
  Images,
  MapView,
  ShapeSource,
  StyleURL,
  SymbolLayer,
} from '@rnmapbox/maps';

const point = {
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      id: 'p1',
      properties: {},
      geometry: { type: 'Point', coordinates: [-74.006, 40.7128] },
    },
  ],
};

export default function ImagesColdStyleLoadRepro() {
  return (
    <MapView style={styles.map} styleURL={StyleURL.Light}>
      <Images onImageMissing={(key) => console.warn('onImageMissing:', key)}>
        <Image name="bubble">
          <View style={styles.bubble} />
        </Image>
      </Images>
      <ShapeSource id="repro-source" shape={point}>
        <SymbolLayer
          id="repro-layer"
          style={{ iconImage: 'bubble', iconAllowOverlap: true }}
        />
      </ShapeSource>
    </MapView>
  );
}

const styles = StyleSheet.create({
  map: { flex: 1 },
  bubble: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#E00' },
});

Observed behavior and steps to reproduce

The <Image> child view is snapshotted and registered into the style, but if the Mapbox style has not finished loading at that moment the registration is silently discarded. The SymbolLayer then renders with no icon, permanently, for the life of that map view.

Steps

  1. Make sure the Mapbox style is not in the ambient cache. Either install the app fresh, or clear just the style cache:
BUNDLE_ID=<your.bundle.id>
C=$(xcrun simctl get_app_container booted "$BUNDLE_ID" data)
xcrun simctl terminate booted "$BUNDLE_ID"
rm -f "$C/Library/Application Support/.mapbox/map_data/map_data.db"*
  1. Cold launch and render the component above.

Observed: no red circle. onImageMissing fires with bubble. Relaunching, now that the style is cached, renders the icon correctly.

Slowing the network (Network Link Conditioner) widens the window and makes it more reliable.

Evidence from our app. Same symptom on 12 view-backed <Image> children behind an iconImage: ['concat', 'callout', ...] expression: our styled text rendered as bare text with no bubble behind them. Five controlled runs:

Run Style in ambient cache Workaround Result
1 absent (fresh install) none broken, onImageMissing fires for every requested name
2 present but cache expired 4 min none healthy, no onImageMissing at all
3 deleted Image.refresh() from onImageMissing onImageMissing fires, then icons render
4 deleted none broken
5 absent (fresh install) waitForStyleLoad() -> true patch healthy, no onImageMissing at all

Runs 3 and 4 differ only in whether refresh() was called, which shows the images are recoverable and were simply never registered. Runs 2 and 4 differ only in whether the style was cached, which isolates the trigger to style load latency.

Two further details that may help:

Expected behavior

Images declared as <Image> children of <Images> should end up in the style regardless of whether the style had finished loading when the component mounted, the same as every other style-mutating component.

RNMBXLayer (RNMBXLayer.swift:115) and RNMBXInteractiveElement (RNMBXInteractiveElement.swift:68) both return waitForStyleLoad() == true. RNMBXImages is the only one that does not.

Notes / preliminary analysis

RNMBXImages does not implement waitForStyleLoad(), so it inherits the false default from the RNMBXMapComponentProtocol extension (RNMBXCamera.swift:22-26). RNMBXMapView.addToMap therefore attaches it immediately, with a style reference captured before .styleLoaded:

  1. addToMap runs inside withMapView { }, so it fires as soon as the Mapbox view exists.
  2. addImageViews sets RNMBXImage.images, whose didSet schedules setImage().
  3. setImage() snapshots the child view and calls _addImageToStyle() -> RNMBXImages.addImage -> style.addImage(...).
  4. That call is wrapped in logged(...) (RNMBXImages.swift:284), so if the style is not loaded the throw is swallowed and the image is never registered.
  5. Nothing retries. addFeaturesToMap (RNMBXMapView.swift:966) only re-adds entries whose addedToMap is false, and this entry was already marked true by the early attach.

Android is unaffected because RNMBXImage.kt:36-44 re-snapshots on every onLayoutChange. iOS has no equivalent retry, and Image.refresh() (#4249) is the only recovery, which requires app code to react to onImageMissing.

Suggested fix

--- a/ios/RNMBX/RNMBXImages.swift
+++ b/ios/RNMBX/RNMBXImages.swift
@@ -68,7 +68,10 @@ open class RNMBXImages : UIView, RNMBXMapComponent {
   }

   // MARK: - RNMBXMapComponent
-  // Uses default implementation from RNMBXMapComponentProtocol extension (returns false)
+
+  public func waitForStyleLoad() -> Bool {
+    return true
+  }

   public func addToMap(_ map: RNMBXMapView, style: Style) {
     self.style = style

This defers registration into the onEvery(event: .styleLoaded) handler at RNMBXMapView.swift:1229, which calls addFeaturesToMap on every style load. As a side benefit, images are then re-registered after a style swap instead of being dropped.

Verified against 10.3.5 as a pnpm patch: run 5 above, fresh install with no ambient cache, which is the condition that reproduced 100% of the time before.

Additional links and references

Activity

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

    bug 🪲Something isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions