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
- 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"*
- 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:
addToMap runs inside withMapView { }, so it fires as soon as the Mapbox view exists.
addImageViews sets RNMBXImage.images, whose didSet schedules setImage().
setImage() snapshots the child view and calls _addImageToStyle() -> RNMBXImages.addImage -> style.addImage(...).
- 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.
- 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
Mapbox Version
11.29.0-rc.1
React Native Version
0.86.2
Platform
iOS
@rnmapbox/mapsversion10.3.5
Standalone component to reproduce
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. TheSymbolLayerthen renders with no icon, permanently, for the life of that map view.Steps
Observed: no red circle.
onImageMissingfires withbubble. 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 aniconImage: ['concat', 'callout', ...]expression: our styled text rendered as bare text with no bubble behind them. Five controlled runs:onImageMissingfires for every requested nameonImageMissingat allImage.refresh()fromonImageMissingonImageMissingfires, then icons renderwaitForStyleLoad() -> truepatchonImageMissingat allRuns 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:
onImageMissingfiring at all provesRNMBXImages.addToMaphad run, so this is not the Fabric view-flattening class of bug fixed in fix(PointAnnotation): fix nested children not rendering on New Architecture (Fabric) #4231.styleImageMissingarrives 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) andRNMBXInteractiveElement(RNMBXInteractiveElement.swift:68) both returnwaitForStyleLoad() == true.RNMBXImagesis the only one that does not.Notes / preliminary analysis
RNMBXImagesdoes not implementwaitForStyleLoad(), so it inherits thefalsedefault from theRNMBXMapComponentProtocolextension (RNMBXCamera.swift:22-26).RNMBXMapView.addToMaptherefore attaches it immediately, with a style reference captured before.styleLoaded:addToMapruns insidewithMapView { }, so it fires as soon as the Mapbox view exists.addImageViewssetsRNMBXImage.images, whosedidSetschedulessetImage().setImage()snapshots the child view and calls_addImageToStyle()->RNMBXImages.addImage->style.addImage(...).logged(...)(RNMBXImages.swift:284), so if the style is not loaded the throw is swallowed and the image is never registered.addFeaturesToMap(RNMBXMapView.swift:966) only re-adds entries whoseaddedToMapisfalse, and this entry was already markedtrueby the early attach.Android is unaffected because
RNMBXImage.kt:36-44re-snapshots on everyonLayoutChange. iOS has no equivalent retry, andImage.refresh()(#4249) is the only recovery, which requires app code to react toonImageMissing.Suggested fix
This defers registration into the
onEvery(event: .styleLoaded)handler atRNMBXMapView.swift:1229, which callsaddFeaturesToMapon 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
pnpmpatch: run 5 above, fresh install with no ambient cache, which is the condition that reproduced 100% of the time before.Additional links and references
fix(ios): Image refresh) is the closest existing work. It added the manualrefresh()escape hatch for incomplete snapshots. This issue is about registration being dropped entirely, whichrefresh()can recover from, but only if the app wires uponImageMissing.<Image>not updating on iOS) looks like it may share this root cause.fix(PointAnnotation)) is a different Fabric bug, ruled out here becauseonImageMissingfires, which proves the component was attached to the map.