From ea578b73e2a68b45e7be535fb28e2025963a0bd1 Mon Sep 17 00:00:00 2001 From: Jovanni Lo Date: Wed, 17 Jun 2026 02:51:47 +0800 Subject: [PATCH 1/2] fix(ios): prevent crash on Google Maps gradient polyline updates Clear GMSPolyline spans before swapping a shorter path so Google Maps never rebuilds span models against missing segments (GMSModelStyleBuilder couldNotInstantiate -> EXC_BAD_ACCESS). Guard zero-segment paths and drop duplicate adjacent coordinates in both animated and static updates. Gradient rendering is preserved. --- ios/core/GMSPolylineAnimator.m | 55 ++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/ios/core/GMSPolylineAnimator.m b/ios/core/GMSPolylineAnimator.m index 3b2c7c7..a4e49a4 100644 --- a/ios/core/GMSPolylineAnimator.m +++ b/ios/core/GMSPolylineAnimator.m @@ -148,13 +148,25 @@ - (void)update { } GMSMutablePath *path = [GMSMutablePath path]; - for (CLLocation *location in self.coordinates) { - [path addCoordinate:location.coordinate]; + CLLocationCoordinate2D lastCoord = self.coordinates.firstObject.coordinate; + [path addCoordinate:lastCoord]; + for (NSUInteger i = 1; i < self.coordinates.count; i++) { + CLLocationCoordinate2D coord = self.coordinates[i].coordinate; + if (coord.latitude == lastCoord.latitude && coord.longitude == lastCoord.longitude) { + continue; + } + [path addCoordinate:coord]; + lastCoord = coord; } + + // Clear stale spans before swapping the path so Google Maps never rebuilds + // span models against a path with fewer segments (GMSModelStyleBuilder + // couldNotInstantiate -> EXC_BAD_ACCESS). Reassign spans once the new path is set. + _polyline.spans = nil; _polyline.path = path; - if (self.strokeColors.count > 1) { - _polyline.spans = [self createGradientSpans]; + if (path.count > 1 && self.strokeColors.count > 1) { + _polyline.spans = [self createGradientSpansForSegmentCount:path.count - 1]; } else { _polyline.strokeColor = self.strokeColors.firstObject ?: [UIColor blackColor]; } @@ -222,35 +234,44 @@ - (void)updateAnimatedPolyline { } if (headDist <= tailDist) { + _polyline.spans = nil; _polyline.path = [GMSMutablePath path]; return; } - CGFloat visibleLength = headDist - tailDist; NSUInteger startIndex = [self indexForDistance:tailDist]; NSUInteger endIndex = [self indexForDistance:headDist]; GMSMutablePath *path = [GMSMutablePath path]; - NSMutableArray *spans = [NSMutableArray array]; - CLLocationCoordinate2D startCoord = [self coordinateAtDistance:tailDist]; - [path addCoordinate:startCoord]; + CLLocationCoordinate2D lastCoord = [self coordinateAtDistance:tailDist]; + [path addCoordinate:lastCoord]; for (NSUInteger i = startIndex + 1; i <= endIndex; i++) { - [path addCoordinate:self.coordinates[i].coordinate]; + CLLocationCoordinate2D coord = self.coordinates[i].coordinate; + if (coord.latitude == lastCoord.latitude && coord.longitude == lastCoord.longitude) { + continue; + } + [path addCoordinate:coord]; + lastCoord = coord; } CLLocationCoordinate2D endCoord = [self coordinateAtDistance:headDist]; - CLLocationCoordinate2D lastAdded = - (endIndex < self.coordinates.count) ? self.coordinates[endIndex].coordinate : endCoord; - if (endCoord.latitude != lastAdded.latitude || endCoord.longitude != lastAdded.longitude) { + if (endCoord.latitude != lastCoord.latitude || endCoord.longitude != lastCoord.longitude) { [path addCoordinate:endCoord]; } NSUInteger pathCount = path.count; - NSUInteger segmentCount = pathCount - 1; + NSUInteger segmentCount = (pathCount > 1) ? pathCount - 1 : 0; + + if (segmentCount == 0) { + _polyline.spans = nil; + _polyline.path = path; + return; + } if (self.strokeColors.count <= 1) { + _polyline.spans = nil; _polyline.path = path; _polyline.strokeColor = self.strokeColors.firstObject ?: [UIColor blackColor]; return; @@ -259,6 +280,7 @@ - (void)updateAnimatedPolyline { NSUInteger spanCount = MIN(segmentCount, kMaxAnimationSpans); double segmentsPerSpan = (double)segmentCount / spanCount; + NSMutableArray *spans = [NSMutableArray array]; for (NSUInteger i = 0; i < spanCount; i++) { CGFloat gradientPos = ((CGFloat)i + 0.5) / spanCount; UIColor *color = [self colorAtGradientPosition:gradientPos]; @@ -266,13 +288,16 @@ - (void)updateAnimatedPolyline { [spans addObject:[GMSStyleSpan spanWithStyle:style segments:segmentsPerSpan]]; } + // Clear stale spans before swapping the path so Google Maps never rebuilds + // span models against a path with fewer segments (GMSModelStyleBuilder + // couldNotInstantiate -> EXC_BAD_ACCESS). Reassign spans once the new path is set. + _polyline.spans = nil; _polyline.path = path; _polyline.spans = spans; } -- (NSArray *)createGradientSpans { +- (NSArray *)createGradientSpansForSegmentCount:(NSUInteger)segmentCount { NSMutableArray *spans = [NSMutableArray array]; - NSUInteger segmentCount = self.coordinates.count - 1; NSUInteger spanCount = MIN(segmentCount, kMaxGradientSpans); double segmentsPerSpan = (double)segmentCount / spanCount; From d4f880a56b267591800fe5e5d9202abcc16decd8 Mon Sep 17 00:00:00 2001 From: Jovanni Lo Date: Wed, 17 Jun 2026 03:05:29 +0800 Subject: [PATCH 2/2] test(example): drive gradient polyline from truck position Slice the animated route ahead of the moving truck so the polyline coordinates mutate mid-animation, mirroring the live-route scenario from the Mover crash. Bumps example Podfile.lock to beta.16. --- example/bare/ios/Podfile.lock | 4 ++-- example/shared/src/components/CrewMarker.tsx | 6 ++++++ example/shared/src/components/Map.tsx | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/example/bare/ios/Podfile.lock b/example/bare/ios/Podfile.lock index 2f0387a..dff7863 100644 --- a/example/bare/ios/Podfile.lock +++ b/example/bare/ios/Podfile.lock @@ -6,7 +6,7 @@ PODS: - hermes-engine (250829098.0.10): - hermes-engine/Pre-built (= 250829098.0.10) - hermes-engine/Pre-built (250829098.0.10) - - LuggMaps (1.0.0-beta.14): + - LuggMaps (1.0.0-beta.16): - GoogleMaps - hermes-engine - RCTRequired @@ -2427,7 +2427,7 @@ SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 hermes-engine: a43fcac5345a0a468667778019547c5fd282c6e2 - LuggMaps: b69f00e83f91bd2e00fb32341dff2d2cc57e794a + LuggMaps: 7f6539e8b1f2d1c328f6c9634cc0544a78b39076 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 diff --git a/example/shared/src/components/CrewMarker.tsx b/example/shared/src/components/CrewMarker.tsx index 184661b..e6c39b5 100644 --- a/example/shared/src/components/CrewMarker.tsx +++ b/example/shared/src/components/CrewMarker.tsx @@ -21,6 +21,7 @@ interface CrewMarkerProps { loaded?: boolean; speed?: number; zoom?: number; + onSegment?: (index: number) => void; } const getBearing = (from: Coordinate, to: Coordinate, currentBearing = 0) => { @@ -57,7 +58,10 @@ export const CrewMarker = ({ loaded = false, speed = 1, zoom = BASE_ZOOM, + onSegment, }: CrewMarkerProps) => { + const onSegmentRef = useRef(onSegment); + onSegmentRef.current = onSegment; const latitude = useSharedValue(route[0]?.latitude ?? 0); const longitude = useSharedValue(route[0]?.longitude ?? 0); const bearingValue = useSharedValue(0); @@ -102,6 +106,8 @@ export const CrewMarker = ({ return; } + onSegmentRef.current?.(index); + const from = route[index]!; const to = route[index + 1]!; diff --git a/example/shared/src/components/Map.tsx b/example/shared/src/components/Map.tsx index 35f5117..06938ab 100644 --- a/example/shared/src/components/Map.tsx +++ b/example/shared/src/components/Map.tsx @@ -310,6 +310,12 @@ export const Map = memo( [markers] ); + const [truckIndex, setTruckIndex] = useState(0); + const routeAhead = useMemo( + () => smoothedRoute.slice(truckIndex), + [smoothedRoute, truckIndex] + ); + const centerPinStyle = useAnimatedStyle(() => { const bottom = animatedPosition ? screenHeight - animatedPosition.value @@ -353,8 +359,12 @@ export const Map = memo( provider === 'apple' || Platform.OS === 'android' ) )} - - + +