diff --git a/docs/content/docs/components/map-view.mdx b/docs/content/docs/components/map-view.mdx index dc3e5c5..12d760e 100644 --- a/docs/content/docs/components/map-view.mdx +++ b/docs/content/docs/components/map-view.mdx @@ -117,7 +117,8 @@ Notes: instead of rendering the map again (markers and shapes stay live and 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. + image and re-renders, and insets set with `setEdgeInsets` give way to + the `edgeInsets` prop. 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 @@ -324,6 +325,11 @@ interface SetEdgeInsetsOptions { } ``` +Insets set this way stay in place when the map re-renders or other props +change. The last write wins: a new `edgeInsets` prop value replaces them +(instantly, also during an animation), and a later `setEdgeInsets` call +replaces the prop. + ### reload Load the map again, e.g. to recover from missing tiles after a network diff --git a/ios/LuggMapView.mm b/ios/LuggMapView.mm index 2147237..27193a4 100644 --- a/ios/LuggMapView.mm +++ b/ios/LuggMapView.mm @@ -69,7 +69,7 @@ @implementation LuggMapView { double _minZoom; double _maxZoom; UIEdgeInsets _edgeInsets; - UIEdgeInsets _oldEdgeInsets; + UIEdgeInsets _edgeInsetsProp; LuggMapViewInsetAdjustment _insetAdjustment; } @@ -284,9 +284,11 @@ - (void)startStaticContent { // staticKey identifies the map's content; when it changes (e.g. a recycled // list row bound to a new item), any live warmup or displayed snapshot is // stale and the static lifecycle restarts. Without this, an in-flight -// warmup caches the old item's map under the new key. +// warmup caches the old item's map under the new key. Insets from the +// setEdgeInsets command belong to the old item, so the prop insets return. - (void)resetStaticContent { _staticSnapshotDone = NO; + _edgeInsets = _edgeInsetsProp; if (_provider) { [_provider destroy]; @@ -303,6 +305,8 @@ - (void)prepareForRecycle { [super prepareForRecycle]; _staticSnapshotDone = NO; + _edgeInsets = UIEdgeInsetsZero; + _edgeInsetsProp = UIEdgeInsetsZero; [_provider destroy]; _provider = nil; @@ -546,10 +550,15 @@ - (void)updateProps:(Props::Shared const &)props UIEdgeInsets newEdgeInsets = UIEdgeInsetsMake( newViewProps.edgeInsets.top, newViewProps.edgeInsets.left, newViewProps.edgeInsets.bottom, newViewProps.edgeInsets.right); - if (!UIEdgeInsetsEqualToEdgeInsets(newEdgeInsets, _edgeInsets)) { - _oldEdgeInsets = _edgeInsets; - _edgeInsets = newEdgeInsets; - [_provider setEdgeInsets:_edgeInsets oldEdgeInsets:_oldEdgeInsets]; + // Compare with the last prop, not _edgeInsets: the setEdgeInsets command + // writes _edgeInsets, and unrelated prop updates carry the old edgeInsets + if (!UIEdgeInsetsEqualToEdgeInsets(newEdgeInsets, _edgeInsetsProp)) { + _edgeInsetsProp = newEdgeInsets; + [self setEdgeInsets:newEdgeInsets.top + left:newEdgeInsets.left + bottom:newEdgeInsets.bottom + right:newEdgeInsets.right + duration:0]; } [super updateProps:props oldProps:oldProps]; diff --git a/ios/core/AppleMapProvider.mm b/ios/core/AppleMapProvider.mm index 789f2dc..4bce1dc 100644 --- a/ios/core/AppleMapProvider.mm +++ b/ios/core/AppleMapProvider.mm @@ -490,6 +490,11 @@ - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets duration:(double)duration { + // An interrupted animation leaves the map at its in-flight insets, not at + // the caller's last target + if (_edgeInsetsDisplayLink) { + oldEdgeInsets = _edgeInsetsCurrent; + } [self stopEdgeInsetsAnimation]; if (duration != 0 && _mapView) { diff --git a/skills/maps-usage/references/api.md b/skills/maps-usage/references/api.md index e8a6e48..e9ed9e3 100644 --- a/skills/maps-usage/references/api.md +++ b/skills/maps-usage/references/api.md @@ -26,7 +26,7 @@ All methods are synchronous fire-and-forget (`void`). They no-op until the nativ |--------|-----------|-------------| | `moveCamera` | `(coordinate: Coordinate, options?: { zoom?: number; duration?: number }) => void` | Center on a coordinate. `zoom` omitted/`0` keeps the current zoom. Heading and pitch are preserved | | `fitCoordinates` | `(coordinates: Coordinate[], options?: { padding?: EdgeInsets; duration?: number }) => void` | Fit all points in view. Resets heading to north. `padding` is added on top of the current `edgeInsets`. A single coordinate delegates to `moveCamera` at `initialZoom`. Empty array is a no-op | -| `setEdgeInsets` | `(edgeInsets: EdgeInsets, options?: { duration?: number }) => void` | Shift the viewport (logical center, attribution, controls). Camera events report the inset-adjusted center | +| `setEdgeInsets` | `(edgeInsets: EdgeInsets, options?: { duration?: number }) => void` | Shift the viewport (logical center, attribution, controls). Camera events report the inset-adjusted center. Survives re-renders; last write wins vs. the `edgeInsets` prop (a changed prop value replaces it, a later call replaces the prop) | | `reload` | `() => void` | Rebuild the map at the current coordinate and zoom. Live map: heading/pitch reset, `onReady` fires again, children remain. Static map: re-renders the base map and drops the cached `staticKey` image (iOS) or recreates the lite map (Android). Preserves `mapType` and edge insets | `duration` (ms): `-1` (default) = platform default animation, `0` = instant, `> 0` = custom. Static maps ignore it. diff --git a/skills/maps-usage/references/configuration.md b/skills/maps-usage/references/configuration.md index 63f24a6..13eb662 100644 --- a/skills/maps-usage/references/configuration.md +++ b/skills/maps-usage/references/configuration.md @@ -134,7 +134,7 @@ export function App() { | `initialZoom` | `number` | `10` | πŸŽπŸ…ΆπŸ€–πŸŒ | Starting zoom (Google-style levels). Also the zoom `fitCoordinates` uses for a single coordinate | | `minZoom` | `number` | β€” | πŸŽπŸ…ΆπŸ€–πŸŒ | Lower zoom clamp | | `maxZoom` | `number` | β€” | πŸŽπŸ…ΆπŸ€–πŸŒ | Upper zoom clamp | -| `edgeInsets` | `EdgeInsets` | β€” | πŸŽπŸ…ΆπŸ€–πŸŒ | Viewport insets. Shifts the logical center, attribution, and controls. Prefer `setEdgeInsets()` for animated changes | +| `edgeInsets` | `EdgeInsets` | β€” | πŸŽπŸ…ΆπŸ€–πŸŒ | Viewport insets. Shifts the logical center, attribution, and controls. Prefer `setEdgeInsets()` for animated changes. Applied only when the value changes, so re-renders keep `setEdgeInsets()` insets | | `insetAdjustment` | `'never' \| 'automatic'` | `'never'` | πŸŽπŸ…ΆπŸ€– | `'automatic'` adds safe-area insets to the map padding | ### Gestures diff --git a/skills/maps-usage/references/troubleshooting.md b/skills/maps-usage/references/troubleshooting.md index 5545456..104820c 100644 --- a/skills/maps-usage/references/troubleshooting.md +++ b/skills/maps-usage/references/troubleshooting.md @@ -15,6 +15,7 @@ Common issues with `@lugg/maps`, organized by symptom. - [Marker animation janks or re-renders every frame](#marker-animation-janks-or-re-renders-every-frame) - [Overlapping markers stack wrong](#overlapping-markers-stack-wrong) - [Center pin / selected coordinate is offset under a sheet](#center-pin--selected-coordinate-is-offset-under-a-sheet) +- [Sheet insets reset when another prop changes](#sheet-insets-reset-when-another-prop-changes) - [`e.nativeEvent.dragging` is undefined](#enativeeventdragging-is-undefined) - [User location dot never appears](#user-location-dot-never-appears) - [Static maps in a list are slow or crash the app](#static-maps-in-a-list-are-slow-or-crash-the-app) @@ -118,6 +119,14 @@ Common issues with `@lugg/maps`, organized by symptom. **Fix:** Keep the map full-size and push the viewport with `setEdgeInsets({ bottom: sheetHeight })`. Camera events then report the inset-adjusted center, and a pin translated up by `bottom / 2` sits exactly on it. See [Bottom sheet pattern](./advanced-patterns.md#bottom-sheet-with-edge-insets-and-center-pin). +## Sheet insets reset when another prop changes + +**Symptom:** After `setEdgeInsets`, the map snaps back (content slides under the sheet or header) when `theme`, `mapType`, `userLocationEnabled`, or another prop changes, or on any re-render (web). + +**Cause:** `@lugg/maps` ≀ 1.1.3 on iOS and web re-applied the unchanged `edgeInsets` prop over the command's insets. + +**Fix:** Upgrade. Now the last write wins: only a changed `edgeInsets` value replaces command insets. On older versions, drive the sheet insets through the `edgeInsets` prop instead of the command. + ## `e.nativeEvent.dragging` is undefined **Cause:** The camera payload field is `gesture`, not `dragging`. diff --git a/src/MapView.web.tsx b/src/MapView.web.tsx index 4c19776..d8b5cd2 100644 --- a/src/MapView.web.tsx +++ b/src/MapView.web.tsx @@ -131,6 +131,7 @@ export const MapView = memo( const isDraggingRef = useRef(false); const wasGesture = useRef(false); const prevEdgeInsets = useRef(edgeInsets); + const prevEdgeInsetsProp = useRef(edgeInsets); // Bumped by reload() to remount the map at the camera captured then const [reloadState, setReloadState] = useState<{ key: number; @@ -300,7 +301,9 @@ export const MapView = memo( useEffect(() => { if (!map || !edgeInsets) return; - const prev = prevEdgeInsets.current; + // Compare with the last prop, not prevEdgeInsets: the setEdgeInsets + // ref method writes prevEdgeInsets, and re-renders carry the old prop + const prev = prevEdgeInsetsProp.current; const changed = prev?.top !== edgeInsets.top || prev?.left !== edgeInsets.left || @@ -308,6 +311,7 @@ export const MapView = memo( prev?.right !== edgeInsets.right; if (changed) { + prevEdgeInsetsProp.current = edgeInsets; applyEdgeInsets(edgeInsets); } }, [map, edgeInsets, applyEdgeInsets]);