From 3439ba0ab7fdbcacc56fa27b28eb8a814f79bcfa Mon Sep 17 00:00:00 2001 From: Jovanni Lo Date: Fri, 11 Sep 2026 01:49:07 +0800 Subject: [PATCH 1/5] fix(ios): cache static snapshots only once the map is stable mapViewDidFinishTileRendering also fires for tiles that failed permanently, so a blank render could be cached under staticKey with no way to recover. Swap and cache only on mapViewSnapshotReady. --- docs/content/docs/components/map-view.mdx | 5 ++++ ios/core/GoogleStaticMapProvider.mm | 36 +++++++++++++---------- src/MapView.types.ts | 3 +- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/docs/content/docs/components/map-view.mdx b/docs/content/docs/components/map-view.mdx index 4544e38..3b67d7f 100644 --- a/docs/content/docs/components/map-view.mdx +++ b/docs/content/docs/components/map-view.mdx @@ -115,6 +115,11 @@ Notes: re-render from props). The camera and map settings are part of the cache key automatically. Changing `staticKey` discards the current image and re-renders. The cache is bounded by count and total memory. +- On iOS (Google), the base map is only captured and cached once the SDK + reports the map stable with all tiles loaded. If tiles fail (e.g. + offline), the live warmup map stays in place instead and renders again + the next time the row appears, so a bad render can't get stuck behind + `staticKey`. - Alternatively, keeping rows mounted (larger `windowSize` with `removeClippedSubviews`) avoids re-rendering entirely at the cost of holding every row's snapshot in memory. diff --git a/ios/core/GoogleStaticMapProvider.mm b/ios/core/GoogleStaticMapProvider.mm index 3b120ab..57bee29 100644 --- a/ios/core/GoogleStaticMapProvider.mm +++ b/ios/core/GoogleStaticMapProvider.mm @@ -49,7 +49,7 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) { @implementation GoogleStaticMapProvider { GMSMapView *_warmupMapView; - BOOL _tilesRendered; + BOOL _snapshotReady; BOOL _swapScheduled; } @@ -106,7 +106,7 @@ - (void)startWarmup { if (!wrapperView) return; - _tilesRendered = NO; + _snapshotReady = NO; GMSMapView *mapView = DequeueStaticMapView([self poolKey]); if (mapView) { @@ -152,12 +152,12 @@ - (void)destroyWarmupMapView { _warmupMapView = nil; } -// Once tiles are fully rendered, swap the live map with its image and -// release the map view. The image render and map teardown are expensive, -// so they run outside scroll tracking; the live map keeps displaying (and -// loading tiles) until then. +// Once the map is stable, swap the live map with its image and release the +// map view. The image render and map teardown are expensive, so they run +// outside scroll tracking; the live map keeps displaying (and loading +// tiles) until then. - (void)scheduleSwap { - if (_swapScheduled || !_warmupMapView || !_tilesRendered || + if (_swapScheduled || !_warmupMapView || !_snapshotReady || !_warmupMapView.window) return; @@ -176,9 +176,9 @@ - (void)scheduleSwap { - (void)performSwap { // Tiles can invalidate (or the view leave the window) between scheduling - // and this runloop pass; the next tile callback or resumeAnimations + // and this runloop pass; the next snapshotReady or resumeAnimations // retries - if (!_warmupMapView || !_tilesRendered || !_warmupMapView.window) + if (!_warmupMapView || !_snapshotReady || !_warmupMapView.window) return; UIImageView *imageView = [_warmupMapView lugg_snapshotImageView]; @@ -192,20 +192,24 @@ - (void)performSwap { #pragma mark - GMSMapViewDelegate - (void)mapViewDidStartTileRendering:(GMSMapView *)mapView { - _tilesRendered = NO; + _snapshotReady = NO; } +// Fires for tiles that failed permanently too, so it can't gate the swap: +// capturing here cached blank tiles under the static key with no way to +// recover. The visible warmup map has rendered what it can; show the +// overlays now instead of waiting for the swap, which is deferred while +// scrolling - (void)mapViewDidFinishTileRendering:(GMSMapView *)mapView { - _tilesRendered = YES; - // The visible warmup map is fully rendered; show the overlays now - // instead of waiting for the swap, which is deferred while scrolling [self revealOverlays]; - [self scheduleSwap]; } +// Stable per the SDK: tiles loaded, labels and overlays rendered. The only +// signal that the render is complete, so the only one that captures and +// caches. Without it (e.g. offline) the live map stays until the view +// leaves the window, and a fresh warmup runs on return. - (void)mapViewSnapshotReady:(GMSMapView *)mapView { - // Stable per the SDK: tiles loaded, labels and overlays rendered - _tilesRendered = YES; + _snapshotReady = YES; [self revealOverlays]; [self scheduleSwap]; } diff --git a/src/MapView.types.ts b/src/MapView.types.ts index dace9b7..ff3054f 100644 --- a/src/MapView.types.ts +++ b/src/MapView.types.ts @@ -224,7 +224,8 @@ export interface MapViewProps extends ViewProps { * remounted with the same key (and same size, provider, and map settings) * reuses its cached snapshot instead of rendering a live map again - * e.g. set it to your list item's id. The key must uniquely identify the - * map's content, including markers and other children. + * map's content, including markers and other children. Only fully loaded + * renders are cached. * Only used with staticMode. */ staticKey?: string; From 96a87f0d07ff0ced59e157240d7ac6e5f425f0b7 Mon Sep 17 00:00:00 2001 From: Jovanni Lo Date: Fri, 11 Sep 2026 01:49:07 +0800 Subject: [PATCH 2/5] feat: add reload method to MapView Static maps drop the cached snapshot and re-render the base map. Live maps have no tile reload in any SDK, so the native map is recreated at the current camera with children re-added. Web remounts the map. --- .../src/main/java/com/luggmaps/LuggMapView.kt | 32 ++++++++- .../java/com/luggmaps/LuggMapViewManager.kt | 4 ++ .../com/luggmaps/core/GoogleMapProvider.kt | 21 ++++++ .../com/luggmaps/core/MapProviderDelegate.kt | 5 ++ docs/content/docs/components/map-view.mdx | 20 +++++- example/shared/src/components/Map.tsx | 1 + ios/LuggMapView.mm | 70 ++++++++++++++++--- ios/core/AppleMapProvider.mm | 8 +++ ios/core/GoogleMapProvider.mm | 8 +++ ios/core/MapProviderDelegate.h | 4 ++ ios/core/StaticMapProviderBase.h | 5 ++ src/MapView.tsx | 7 ++ src/MapView.types.ts | 7 ++ src/MapView.web.tsx | 23 +++++- src/fabric/LuggMapViewNativeComponent.ts | 8 ++- 15 files changed, 209 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/com/luggmaps/LuggMapView.kt b/android/src/main/java/com/luggmaps/LuggMapView.kt index 4c66b59..f1e118a 100644 --- a/android/src/main/java/com/luggmaps/LuggMapView.kt +++ b/android/src/main/java/com/luggmaps/LuggMapView.kt @@ -131,7 +131,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) : // region Provider Initialization - private fun initializeProvider() { + private fun initializeProvider(latitude: Double = initialLatitude, longitude: Double = initialLongitude, zoom: Float = initialZoom) { if (provider != null || mapWrapperView == null) return if (staticMode) { @@ -146,7 +146,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) : applyProps() - google.initializeMap(mapWrapperView!!, initialLatitude, initialLongitude, initialZoom) + google.initializeMap(mapWrapperView!!, latitude, longitude, zoom) // Flush children mounted before provider was created for (i in 0 until childCount) { @@ -371,6 +371,34 @@ class LuggMapView(private val reactContext: ThemedReactContext) : provider?.fitCoordinates(coordinates, edgeInsetsTop, edgeInsetsLeft, edgeInsetsBottom, edgeInsetsRight, duration) } + // Loads the map again, e.g. to recover from missing tiles. The SDK can't + // reload tiles in place, so the map is recreated at the current camera + // (coordinate and zoom; bearing and tilt reset) with children re-added + fun reload() { + val current = provider ?: return + val latitude = current.cameraLatitude + val longitude = current.cameraLongitude + val zoom = current.cameraZoom + + // Children keep their native marker/overlay objects from the old map; + // detach them so the new provider creates fresh ones on the flush + for (i in 0 until childCount) { + when (val child = getChildAt(i)) { + is LuggMarkerView -> current.removeMarkerView(child) + is LuggPolylineView -> current.removePolylineView(child) + is LuggPolygonView -> current.removePolygonView(child) + is LuggCircleView -> current.removeCircleView(child) + is LuggGroundOverlayView -> current.removeGroundOverlayView(child) + is LuggTileOverlayView -> current.removeTileOverlayView(child) + } + } + current.destroy() + provider = null + if (isAttachedToWindow) { + initializeProvider(latitude, longitude, zoom) + } + } + // endregion companion object { diff --git a/android/src/main/java/com/luggmaps/LuggMapViewManager.kt b/android/src/main/java/com/luggmaps/LuggMapViewManager.kt index 1ef6873..c82c91c 100644 --- a/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +++ b/android/src/main/java/com/luggmaps/LuggMapViewManager.kt @@ -273,6 +273,10 @@ class LuggMapViewManager : ) } + override fun reload(view: LuggMapView) { + view.reload() + } + companion object { const val NAME = "LuggMapView" } diff --git a/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt b/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt index 370ba96..c7e3973 100644 --- a/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +++ b/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt @@ -211,6 +211,9 @@ class GoogleMapProvider(private val context: Context) : groundOverlayToViewMap.clear() markerToViewMap.clear() wrapperView?.touchEventHandler = null + // The wrapper outlives the provider on reload; don't leave the dead map + // view in it + mapView?.let { wrapperView?.removeView(it) } wrapperView = null googleMap?.setOnCameraMoveStartedListener(null) googleMap?.setOnCameraMoveListener(null) @@ -1201,6 +1204,24 @@ class GoogleMapProvider(private val context: Context) : // endregion + // region Camera + + // A static map's camera is inset-shifted (staticCameraTarget), so report + // the requested camera instead + private val liveCameraPosition: CameraPosition? + get() = if (staticMode) null else googleMap?.cameraPosition + + override val cameraLatitude: Double + get() = liveCameraPosition?.target?.latitude ?: initialLatitude + + override val cameraLongitude: Double + get() = liveCameraPosition?.target?.longitude ?: initialLongitude + + override val cameraZoom: Float + get() = liveCameraPosition?.zoom ?: initialZoom + + // endregion + // region Lifecycle override fun pauseAnimations() { diff --git a/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt b/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt index 7c0a132..0b8701c 100644 --- a/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +++ b/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt @@ -58,6 +58,11 @@ interface MapProvider { fun addTileOverlayView(tileOverlayView: LuggTileOverlayView) fun removeTileOverlayView(tileOverlayView: LuggTileOverlayView) + // Current camera (the initial camera until the map is ready) + val cameraLatitude: Double + val cameraLongitude: Double + val cameraZoom: Float + // Lifecycle fun pauseAnimations() fun resumeAnimations() diff --git a/docs/content/docs/components/map-view.mdx b/docs/content/docs/components/map-view.mdx index 3b67d7f..d8b0b76 100644 --- a/docs/content/docs/components/map-view.mdx +++ b/docs/content/docs/components/map-view.mdx @@ -119,7 +119,7 @@ Notes: reports the map stable with all tiles loaded. If tiles fail (e.g. offline), the live warmup map stays in place instead and renders again the next time the row appears, so a bad render can't get stuck behind - `staticKey`. + `staticKey`. To force it, call `reload()` on the ref. - Alternatively, keeping rows mounted (larger `windowSize` with `removeClippedSubviews`) avoids re-rendering entirely at the cost of holding every row's snapshot in memory. @@ -278,6 +278,9 @@ mapRef.current?.setEdgeInsets( { top: 0, left: 0, bottom: 200, right: 0 }, { duration: 300 } ); + +// Load the map again (e.g. after a network outage) +mapRef.current?.reload(); ``` ### moveCamera @@ -318,6 +321,21 @@ interface SetEdgeInsetsOptions { } ``` +### reload + +Load the map again, e.g. to recover from missing tiles after a network +outage. None of the map SDKs can reload tiles in place, so: + +- A static map re-renders its base map. On iOS the cached snapshot for its + `staticKey` is discarded and the new render replaces the current image + when ready; on Android the lite map is recreated. +- A live map is recreated at its current coordinate and zoom (heading and + pitch reset) with all children re-added, and `onReady` fires again. + +```ts +reload(): void +``` + ## Events ### onPress / onLongPress diff --git a/example/shared/src/components/Map.tsx b/example/shared/src/components/Map.tsx index 06938ab..856ce63 100644 --- a/example/shared/src/components/Map.tsx +++ b/example/shared/src/components/Map.tsx @@ -297,6 +297,7 @@ export const Map = memo( moveCamera: (...args) => mapRef.current?.moveCamera(...args), fitCoordinates: (...args) => mapRef.current?.fitCoordinates(...args), setEdgeInsets: (...args) => mapRef.current?.setEdgeInsets(...args), + reload: () => mapRef.current?.reload(), showMarkerCallout: (markerId) => markerRefsMap.current.get(markerId)?.showCallout(), hideMarkerCallout: (markerId) => diff --git a/ios/LuggMapView.mm b/ios/LuggMapView.mm index e661629..c22f6c1 100644 --- a/ios/LuggMapView.mm +++ b/ios/LuggMapView.mm @@ -311,11 +311,20 @@ - (void)prepareForRecycle { #pragma mark - Provider Initialization - (void)initializeProvider { - if (_provider || !_mapWrapperView) - return; - const auto &viewProps = *std::static_pointer_cast(_props); + [self + initializeProviderWithCoordinate:CLLocationCoordinate2DMake( + viewProps.initialCoordinate.latitude, + viewProps.initialCoordinate + .longitude) + zoom:viewProps.initialZoom]; +} + +- (void)initializeProviderWithCoordinate:(CLLocationCoordinate2D)coordinate + zoom:(double)zoom { + if (_provider || !_mapWrapperView) + return; if (_providerType == LuggMapViewProvider::Apple) { _provider = _staticMode ? [[AppleStaticMapProvider alloc] init] @@ -333,13 +342,9 @@ - (void)initializeProvider { _provider.delegate = self; _provider.staticMode = _staticMode; - CLLocationCoordinate2D coordinate = - CLLocationCoordinate2DMake(viewProps.initialCoordinate.latitude, - viewProps.initialCoordinate.longitude); - [_provider initializeMapInView:_mapWrapperView initialCoordinate:coordinate - initialZoom:viewProps.initialZoom]; + initialZoom:zoom]; // After initializeMapInView so the cache key reads the provider's camera; // the base render is async and picks up the cached image @@ -582,6 +587,55 @@ - (void)setEdgeInsets:(double)top duration:duration]; } +// Loads the map again, e.g. to recover from missing tiles. A static map +// drops its cached snapshot and renders the base map again in place. +// Neither SDK can reload a live map's tiles, so the native map view is +// recreated at the current camera (coordinate and zoom; heading and pitch +// reset) with children re-added. +- (void)reload { + if (_staticMode) { + NSString *cacheKey = [self staticSnapshotCacheKey]; + if (cacheKey) { + [StaticSnapshotCache() removeObjectForKey:cacheKey]; + } + if ([_provider isKindOfClass:[StaticMapProviderBase class]]) { + [(StaticMapProviderBase *)_provider rerenderBaseMap]; + } + return; + } + + if (!_provider) + return; + + CLLocationCoordinate2D coordinate = _provider.coordinate; + double zoom = _provider.zoom; + // Children keep their native marker/overlay objects from the old map; + // detach them so the new provider creates fresh ones on the flush + [self removeChildrenFromProvider]; + [_provider destroy]; + _provider = nil; + _initialized = NO; + [self initializeProviderWithCoordinate:coordinate zoom:zoom]; +} + +- (void)removeChildrenFromProvider { + for (UIView *subview in self.subviews) { + if ([subview isKindOfClass:[LuggMarkerView class]]) { + [_provider removeMarkerView:(LuggMarkerView *)subview]; + } else if ([subview isKindOfClass:[LuggPolylineView class]]) { + [_provider removePolylineView:(LuggPolylineView *)subview]; + } else if ([subview isKindOfClass:[LuggPolygonView class]]) { + [_provider removePolygonView:(LuggPolygonView *)subview]; + } else if ([subview isKindOfClass:[LuggCircleView class]]) { + [_provider removeCircleView:(LuggCircleView *)subview]; + } else if ([subview isKindOfClass:[LuggGroundOverlayView class]]) { + [_provider removeGroundOverlayView:(LuggGroundOverlayView *)subview]; + } else if ([subview isKindOfClass:[LuggTileOverlayView class]]) { + [_provider removeTileOverlayView:(LuggTileOverlayView *)subview]; + } + } +} + - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { RCTLuggMapViewHandleCommand(self, commandName, args); } diff --git a/ios/core/AppleMapProvider.mm b/ios/core/AppleMapProvider.mm index 569e6fd..f4b683f 100644 --- a/ios/core/AppleMapProvider.mm +++ b/ios/core/AppleMapProvider.mm @@ -221,6 +221,14 @@ - (void)destroy { _isMapReady = NO; } +- (CLLocationCoordinate2D)coordinate { + return _mapView.centerCoordinate; +} + +- (double)zoom { + return _mapView.zoomLevel; +} + - (void)destroyMapView { [self dismissNonBubbledCallout]; [self stopEdgeInsetsAnimation]; diff --git a/ios/core/GoogleMapProvider.mm b/ios/core/GoogleMapProvider.mm index ac36c68..210d170 100644 --- a/ios/core/GoogleMapProvider.mm +++ b/ios/core/GoogleMapProvider.mm @@ -183,6 +183,14 @@ - (void)destroyMapView { _mapView = nil; } +- (CLLocationCoordinate2D)coordinate { + return _mapView.camera.target; +} + +- (double)zoom { + return _mapView.camera.zoom; +} + #pragma mark - Props - (void)setZoomEnabled:(BOOL)enabled { diff --git a/ios/core/MapProviderDelegate.h b/ios/core/MapProviderDelegate.h index e421319..4d94be9 100644 --- a/ios/core/MapProviderDelegate.h +++ b/ios/core/MapProviderDelegate.h @@ -89,6 +89,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeTileOverlayView:(LuggTileOverlayView *)tileOverlayView; - (void)syncTileOverlayView:(LuggTileOverlayView *)tileOverlayView; +// Current camera (the initial camera until the map is ready) +@property(nonatomic, readonly) CLLocationCoordinate2D coordinate; +@property(nonatomic, readonly) double zoom; + // Lifecycle - (void)pauseAnimations; - (void)resumeAnimations; diff --git a/ios/core/StaticMapProviderBase.h b/ios/core/StaticMapProviderBase.h index f342a66..205c6a1 100644 --- a/ios/core/StaticMapProviderBase.h +++ b/ios/core/StaticMapProviderBase.h @@ -42,6 +42,11 @@ CGPoint LuggStaticPointForCoordinate(MKMapRect mapRect, CGSize size, /// delegate callbacks and marks the base render done. - (void)displayBaseImage:(UIImage *)image fromCache:(BOOL)fromCache; +/// Discards any cached, displayed or in-flight base render and renders +/// again at the current camera. The displayed image stays until the new +/// one replaces it. +- (void)rerenderBaseMap; + /// Shows the marker and shape overlays (idempotent). Called automatically /// when the base image displays; subclasses whose base map is already /// visible earlier (e.g. a live warmup map) can call it sooner. diff --git a/src/MapView.tsx b/src/MapView.tsx index 715a917..12671ea 100644 --- a/src/MapView.tsx +++ b/src/MapView.tsx @@ -80,6 +80,13 @@ export class MapView Commands.setEdgeInsets(ref, top, left, bottom, right, duration); } + reload() { + const ref = this.nativeRef.current; + if (!ref) return; + + Commands.reload(ref); + } + render() { const { provider, diff --git a/src/MapView.types.ts b/src/MapView.types.ts index ff3054f..1f5ece6 100644 --- a/src/MapView.types.ts +++ b/src/MapView.types.ts @@ -135,6 +135,13 @@ export interface MapViewRef { options?: FitCoordinatesOptions ): void; setEdgeInsets(edgeInsets: EdgeInsets, options?: SetEdgeInsetsOptions): void; + /** + * Loads the map again, e.g. to recover from missing tiles. A static map + * re-renders its base map, discarding the cached snapshot. A live map is + * recreated at its current coordinate and zoom (heading and pitch reset) + * and fires onReady again. + */ + reload(): void; } /** diff --git a/src/MapView.web.tsx b/src/MapView.web.tsx index 5a01144..7fd13d9 100644 --- a/src/MapView.web.tsx +++ b/src/MapView.web.tsx @@ -131,6 +131,12 @@ export const MapView = memo( const isDraggingRef = useRef(false); const wasGesture = useRef(false); const prevEdgeInsets = useRef(edgeInsets); + // Bumped by reload() to remount the map at the camera captured then + const [reloadState, setReloadState] = useState<{ + key: number; + center?: google.maps.LatLngLiteral; + zoom?: number; + }>({ key: 0 }); const offsetCenter = useCallback( ( @@ -268,6 +274,18 @@ export const MapView = memo( ) { applyEdgeInsets(newEdgeInsets, options?.duration); }, + + // The JS SDK can't reload tiles in place; remount the map at the + // current camera + reload() { + if (!map) return; + + setReloadState((state) => ({ + key: state.key + 1, + center: map.getCenter()?.toJSON(), + zoom: map.getZoom(), + })); + }, }), [map, initialZoom, offsetCenter, applyEdgeInsets] ); @@ -481,10 +499,11 @@ export const MapView = memo( > void; + reload: (viewRef: React.ElementRef) => void; } export const Commands = codegenNativeCommands({ - supportedCommands: ['moveCamera', 'fitCoordinates', 'setEdgeInsets'], + supportedCommands: [ + 'moveCamera', + 'fitCoordinates', + 'setEdgeInsets', + 'reload', + ], }); export default codegenNativeComponent( From 7cd20dd33ed6c0696632f57c893e9f60c4f1ac9b Mon Sep 17 00:00:00 2001 From: Jovanni Lo Date: Fri, 11 Sep 2026 01:49:10 +0800 Subject: [PATCH 3/5] chore(example): add reload use cases --- example/shared/src/screens/HomeScreen.tsx | 1 + .../shared/src/screens/StaticMapsScreen.tsx | 24 ++++++++++++++++++- example/shared/src/sheets/ControlSheet.tsx | 8 +++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/example/shared/src/screens/HomeScreen.tsx b/example/shared/src/screens/HomeScreen.tsx index 7d87e6a..0accb9f 100644 --- a/example/shared/src/screens/HomeScreen.tsx +++ b/example/shared/src/screens/HomeScreen.tsx @@ -279,6 +279,7 @@ const HomeContent = ({ onClearMarkers={clear} onMoveCamera={moveToRandomMarker} onFitMarkers={fitAllMarkers} + onReload={() => mapRef.current?.reload()} onToggleMap={() => setShowMap((prev) => !prev)} onToggleProvider={() => setProvider((p) => (p === 'google' ? 'apple' : 'google')) diff --git a/example/shared/src/screens/StaticMapsScreen.tsx b/example/shared/src/screens/StaticMapsScreen.tsx index 17f1726..85401e5 100644 --- a/example/shared/src/screens/StaticMapsScreen.tsx +++ b/example/shared/src/screens/StaticMapsScreen.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { FlatList, Platform, @@ -262,6 +262,7 @@ const PlaceCard = ({ }) => { const { colors } = useTheme(); const { width } = useWindowDimensions(); + const mapRef = useRef(null); const camera = fittedCamera( placeCoordinates(place), @@ -281,6 +282,7 @@ const PlaceCard = ({ > + {/* Re-renders the base map, dropping its cached snapshot - e.g. to + recover a map that rendered with missing tiles */} + [ + styles.reload, + { backgroundColor: colors.backgroundElevated }, + pressed && styles.cardPressed, + ]} + onPress={() => mapRef.current?.reload()} + > + Reload + {place.name} @@ -361,6 +375,14 @@ const styles = StyleSheet.create({ map: { height: MAP_HEIGHT, }, + reload: { + position: 'absolute', + top: sizes.sm, + right: sizes.sm, + paddingHorizontal: sizes.md, + paddingVertical: sizes.xs, + borderRadius: sizes.radiusFull, + }, cardContent: { padding: sizes.lg, gap: sizes.xs, diff --git a/example/shared/src/sheets/ControlSheet.tsx b/example/shared/src/sheets/ControlSheet.tsx index 4d0577f..0b770f0 100644 --- a/example/shared/src/sheets/ControlSheet.tsx +++ b/example/shared/src/sheets/ControlSheet.tsx @@ -29,6 +29,7 @@ interface ControlSheetProps { onClearMarkers: () => void; onMoveCamera: () => void; onFitMarkers: () => void; + onReload: () => void; onToggleMap: () => void; onToggleProvider: () => void; onLoadGeojson: () => void; @@ -56,6 +57,7 @@ export const ControlSheet = forwardRef( onClearMarkers, onMoveCamera, onFitMarkers, + onReload, onToggleMap, onToggleProvider, onLoadGeojson, @@ -122,6 +124,12 @@ export const ControlSheet = forwardRef( onPress={onFitMarkers} disabled={markerCount === 0} /> +