rnmapbox / rnmapbox/maps

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

Open Beginner friendly
#4,287 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug 🪲
Dominant language
Kotlin
Stars
2.9k
Forks
947
Avg merge
6d 37m
Merged PRs (30d)
1

Description

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:

  • onImageMissing firing at all proves RNMBXImages.addToMap had run, so this is not the Fabric view-flattening class of bug fixed in #4231.
  • styleImageMissing arrives after .styleLoaded (70ms after, in our capture), so by the time the layer draws the style is fully loaded and the image is simply absent from it.
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
  • #4249 (fix(ios): Image refresh) is the closest existing work. It added the manual refresh() escape hatch for incomplete snapshots. This issue is about registration being dropped entirely, which refresh() can recover from, but only if the app wires up onImageMissing.
  • #3674 (async images under <Image> not updating on iOS) looks like it may share this root cause.
  • #4231 (fix(PointAnnotation)) is a different Fabric bug, ruled out here because onImageMissing fires, which proves the component was attached to the map.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read ios/RNMBX/RNMBXImages.swift alongside the default waitForStyleLoad implementation in RNMBXCamera.swift and the style-load handling in RNMBXMapView.swift. Run the cold-style-load reproduction described in the issue, then verify that view-backed images register after style loading and no longer trigger onImageMissing.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, swift
Domain
mobile-dev
Issue type
Bug
Difficulty
1/5
Estimated time
Under an hour
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
90/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.