[Bug]: iOS: view-backed <Image> icons are silently dropped when the style is still loading (Images does not waitForStyleLoad)
Nobody has claimed this yet.
- 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
- 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:
onImageMissingfiring at all provesRNMBXImages.addToMaphad run, so this is not the Fabric view-flattening class of bug fixed in #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) 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:
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(...).- 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 whoseaddedToMapisfalse, and this entry was already markedtrueby 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 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. - #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 becauseonImageMissingfires, which proves the component was attached to the map.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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