From d961e5255156c299aa93b62e2ee9ca5bc1b9a7bf Mon Sep 17 00:00:00 2001 From: Eric Kreutzer Date: Sun, 18 Oct 2026 06:40:35 -0600 Subject: [PATCH 1/2] fix(ios): keep setEdgeInsets insets when other props change updateProps compared the edgeInsets prop with _edgeInsets, which the setEdgeInsets command also writes. Fabric copies unchanged props from the previous props, so any unrelated prop update (theme, mapType, userLocationEnabled, ...) carried the old edgeInsets value and re-applied it, instantly undoing insets set by the command. Track the last applied prop in _edgeInsetsProp and apply the prop only when it changes. Reset both values in prepareForRecycle so a recycled view does not inherit the previous map's insets. --- ios/LuggMapView.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ios/LuggMapView.mm b/ios/LuggMapView.mm index 2147237..1dee029 100644 --- a/ios/LuggMapView.mm +++ b/ios/LuggMapView.mm @@ -69,6 +69,7 @@ @implementation LuggMapView { double _minZoom; double _maxZoom; UIEdgeInsets _edgeInsets; + UIEdgeInsets _edgeInsetsProp; UIEdgeInsets _oldEdgeInsets; LuggMapViewInsetAdjustment _insetAdjustment; } @@ -303,6 +304,8 @@ - (void)prepareForRecycle { [super prepareForRecycle]; _staticSnapshotDone = NO; + _edgeInsets = UIEdgeInsetsZero; + _edgeInsetsProp = UIEdgeInsetsZero; [_provider destroy]; _provider = nil; @@ -546,7 +549,10 @@ - (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)) { + // 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; _oldEdgeInsets = _edgeInsets; _edgeInsets = newEdgeInsets; [_provider setEdgeInsets:_edgeInsets oldEdgeInsets:_oldEdgeInsets]; From 2a98b4c0cdada2783bd5ffa7165085c5ed751f5a Mon Sep 17 00:00:00 2001 From: Jovanni Lo Date: Sun, 18 Oct 2026 23:55:09 +0800 Subject: [PATCH 2/2] fix: last write wins between edgeInsets prop and setEdgeInsets - iOS: apply prop changes through setEdgeInsets (duration 0) so a running Apple inset animation stops; drop unused _oldEdgeInsets - iOS Apple: start from in-flight insets when interrupting an animation - iOS static: staticKey change restores prop insets - web: compare edgeInsets prop with last prop, not command-written insets - docs + skill: document prop vs setEdgeInsets precedence --- docs/content/docs/components/map-view.mdx | 8 +++++++- ios/LuggMapView.mm | 13 ++++++++----- ios/core/AppleMapProvider.mm | 5 +++++ skills/maps-usage/references/api.md | 2 +- skills/maps-usage/references/configuration.md | 2 +- skills/maps-usage/references/troubleshooting.md | 9 +++++++++ src/MapView.web.tsx | 6 +++++- 7 files changed, 36 insertions(+), 9 deletions(-) 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 1dee029..27193a4 100644 --- a/ios/LuggMapView.mm +++ b/ios/LuggMapView.mm @@ -70,7 +70,6 @@ @implementation LuggMapView { double _maxZoom; UIEdgeInsets _edgeInsets; UIEdgeInsets _edgeInsetsProp; - UIEdgeInsets _oldEdgeInsets; LuggMapViewInsetAdjustment _insetAdjustment; } @@ -285,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]; @@ -553,9 +554,11 @@ - (void)updateProps:(Props::Shared const &)props // writes _edgeInsets, and unrelated prop updates carry the old edgeInsets if (!UIEdgeInsetsEqualToEdgeInsets(newEdgeInsets, _edgeInsetsProp)) { _edgeInsetsProp = newEdgeInsets; - _oldEdgeInsets = _edgeInsets; - _edgeInsets = newEdgeInsets; - [_provider setEdgeInsets:_edgeInsets oldEdgeInsets:_oldEdgeInsets]; + [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]);