Skip to content

Commit 293e3b9

Browse files
pjleonard37github-actions[bot]
authored andcommitted
Fix crash in Camera API methods with invalid coordinates (#8617)
Fixes crash in camera API methods when given invalid coordinates (NaN or infinity values). These camera methods now catch exceptions and return empty `CameraOptions` instead of crashing the app. GitOrigin-RevId: eadf53dcf2536f639805df0e0d7c42042df198dc
1 parent f4cfeb9 commit 293e3b9

File tree

3 files changed

+166
-26
lines changed

3 files changed

+166
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Mapbox welcomes participation and contributions from everyone.
44

55
## main
66

7+
### Bug fixes 🐞
8+
* Fixed crash in camera API methods when given invalid coordinates (NaN or infinity values). Methods now return empty `CameraOptions` instead of crashing.
9+
710
## 11.18.0-beta.1 - 17 December, 2025
811

912
### Breaking changes ⚠️

Sources/MapboxMaps/Foundation/MapboxMap.swift

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,8 @@ public final class MapboxMap: StyleManager {
539539
/// - pitch: The new pitch to be used by the camera, in degrees (0°, 85°) with 0° being a top-down view.
540540
/// - maxZoom: The maximum zoom level to allow when the camera would transition to the specified bounds.
541541
/// - offset: The center of the given bounds relative to the map's center, measured in points.
542-
/// - Returns: A `CameraOptions` that fits the provided constraints
542+
/// - Returns: A `CameraOptions` that fits the provided constraints. Returns an empty CameraOptions if the
543+
/// calculation fails (e.g., when coordinates are invalid).
543544
/// - Important:
544545
/// The equivalent of the following deprecated call, where `point1` and `point2` are the most southwestern and northeastern points:
545546
/// ```swift
@@ -578,14 +579,21 @@ public final class MapboxMap: StyleManager {
578579
pitch: Double?,
579580
maxZoom: Double?,
580581
offset: CGPoint?) -> CameraOptions {
581-
return CameraOptions.Marshaller.toSwift(
582-
__map.cameraForCoordinateBounds(
583-
for: coordinateBounds,
584-
padding: padding?.toMBXEdgeInsetsValue(),
585-
bearing: bearing?.NSNumber,
586-
pitch: pitch?.NSNumber,
587-
maxZoom: maxZoom?.NSNumber,
588-
offset: offset?.screenCoordinate))
582+
do {
583+
let coreOptions = try NSExceptionHandler.try {
584+
__map.cameraForCoordinateBounds(
585+
for: coordinateBounds,
586+
padding: padding?.toMBXEdgeInsetsValue(),
587+
bearing: bearing?.NSNumber,
588+
pitch: pitch?.NSNumber,
589+
maxZoom: maxZoom?.NSNumber,
590+
offset: offset?.screenCoordinate)
591+
}
592+
return CameraOptions.Marshaller.toSwift(coreOptions)
593+
} catch {
594+
Log.error("camera(for coordinateBounds:) failed with error: \(error). Returning empty CameraOptions.")
595+
return CameraOptions()
596+
}
589597
}
590598

591599
/// Calculates a `CameraOptions` to fit a list of coordinates.
@@ -599,7 +607,8 @@ public final class MapboxMap: StyleManager {
599607
/// If you want to apply padding to the map use `camera` parameter on ``camera(for:camera:coordinatesPadding:maxZoom:offset:)``
600608
/// - bearing: The new bearing to be used by the camera, in degrees (0°, 360°) clockwise from true north.
601609
/// - pitch: The new pitch to be used by the camera, in degrees (0°, 85°) with 0° being a top-down view.
602-
/// - Returns: A `CameraOptions` that fits the provided constraints
610+
/// - Returns: A `CameraOptions` that fits the provided constraints. Returns an empty CameraOptions if the
611+
/// calculation fails (e.g., when coordinates are invalid).
603612
/// - Important:
604613
/// The equivalent of the following deprecated call:
605614
/// ```swift
@@ -635,12 +644,19 @@ public final class MapboxMap: StyleManager {
635644
padding: UIEdgeInsets?,
636645
bearing: Double?,
637646
pitch: Double?) -> CameraOptions {
638-
return CameraOptions.Marshaller.toSwift(
639-
__map.cameraForCoordinates(
640-
for: coordinates.map { Coordinate2D(value: $0) },
641-
padding: padding?.toMBXEdgeInsetsValue(),
642-
bearing: bearing?.NSNumber,
643-
pitch: pitch?.NSNumber))
647+
do {
648+
let coreOptions = try NSExceptionHandler.try {
649+
__map.cameraForCoordinates(
650+
for: coordinates.map { Coordinate2D(value: $0) },
651+
padding: padding?.toMBXEdgeInsetsValue(),
652+
bearing: bearing?.NSNumber,
653+
pitch: pitch?.NSNumber)
654+
}
655+
return CameraOptions.Marshaller.toSwift(coreOptions)
656+
} catch {
657+
Log.error("camera(for coordinates:) failed with error: \(error). Returning empty CameraOptions.")
658+
return CameraOptions()
659+
}
644660
}
645661

646662
/// Calculates a `CameraOptions` to fit a list of coordinates into a sub-rect of the map.
@@ -716,7 +732,8 @@ public final class MapboxMap: StyleManager {
716732
/// - padding: The new padding to be used by the camera.
717733
/// - bearing: The new bearing to be used by the camera.
718734
/// - pitch: The new pitch to be used by the camera.
719-
/// - Returns: A `CameraOptions` that fits the provided constraints
735+
/// - Returns: A `CameraOptions` that fits the provided constraints. Returns an empty CameraOptions if the
736+
/// calculation fails (e.g., when coordinates are invalid).
720737
///
721738
/// - Important:
722739
/// The equivalent of the following deprecated call:
@@ -749,12 +766,19 @@ public final class MapboxMap: StyleManager {
749766
padding: UIEdgeInsets,
750767
bearing: CGFloat?,
751768
pitch: CGFloat?) -> CameraOptions {
752-
return CameraOptions.Marshaller.toSwift(
753-
__map.cameraForGeometry(
754-
for: MapboxCommon.Geometry(geometry),
755-
padding: padding.toMBXEdgeInsetsValue(),
756-
bearing: bearing?.NSNumber,
757-
pitch: pitch?.NSNumber))
769+
do {
770+
let coreOptions = try NSExceptionHandler.try {
771+
__map.cameraForGeometry(
772+
for: MapboxCommon.Geometry(geometry),
773+
padding: padding.toMBXEdgeInsetsValue(),
774+
bearing: bearing?.NSNumber,
775+
pitch: pitch?.NSNumber)
776+
}
777+
return CameraOptions.Marshaller.toSwift(coreOptions)
778+
} catch {
779+
Log.error("camera(for geometry:) failed with error: \(error). Returning empty CameraOptions.")
780+
return CameraOptions()
781+
}
758782
}
759783

760784
// MARK: - CameraOptions to CoordinateBounds
@@ -962,10 +986,19 @@ public final class MapboxMap: StyleManager {
962986
/// - to: The point to which the map is dragged.
963987
///
964988
/// - Returns:
965-
/// The camera options object showing end point.
989+
/// The camera options object showing end point. Returns an empty CameraOptions if the
990+
/// drag calculation fails (e.g., when coordinates are invalid). An empty CameraOptions
991+
/// passed to `setCamera` acts as a no-op, leaving the camera unchanged.
966992
public func dragCameraOptions(from: CGPoint, to: CGPoint) -> CameraOptions {
967-
let options = __map.cameraForDrag(forStart: from.screenCoordinate, end: to.screenCoordinate)
968-
return CameraOptions.Marshaller.toSwift(options)
993+
do {
994+
let coreOptions = try NSExceptionHandler.try {
995+
__map.cameraForDrag(forStart: from.screenCoordinate, end: to.screenCoordinate)
996+
}
997+
return CameraOptions.Marshaller.toSwift(coreOptions)
998+
} catch {
999+
Log.error("dragCameraOptions failed with error: \(error). Returning empty CameraOptions.")
1000+
return CameraOptions()
1001+
}
9691002
}
9701003

9711004
/// :nodoc:

Tests/MapboxMapsTests/Foundation/MapboxMapTests.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,108 @@ final class MapboxMapTests: XCTestCase {
539539
XCTAssertEqual(mapboxMap.viewAnnotationAvoidLayers, layers)
540540
XCTAssertEqual(mapboxMap.__testingMap.getViewAnnotationAvoidLayers(), layers)
541541
}
542+
543+
// MARK: - Exception Handling Tests
544+
545+
func testDragCameraOptionsWithNaNCoordinates() {
546+
// This test verifies that dragCameraOptions catches exceptions from the native layer
547+
// when given invalid coordinates (NaN) and returns empty CameraOptions instead of crashing.
548+
549+
let fromPoint = CGPoint(x: 0, y: 0)
550+
let toPoint = CGPoint(x: Double.nan, y: Double.nan)
551+
552+
let cameraOptions = mapboxMap.dragCameraOptions(from: fromPoint, to: toPoint)
553+
554+
// Should not crash and should return empty CameraOptions (all properties nil)
555+
XCTAssertNotNil(cameraOptions, "dragCameraOptions should not crash with NaN coordinates")
556+
XCTAssertNil(cameraOptions.center, "CameraOptions should be empty after exception")
557+
XCTAssertNil(cameraOptions.zoom, "CameraOptions should be empty after exception")
558+
XCTAssertNil(cameraOptions.bearing, "CameraOptions should be empty after exception")
559+
XCTAssertNil(cameraOptions.pitch, "CameraOptions should be empty after exception")
560+
}
561+
562+
func testDragCameraOptionsWithInfiniteCoordinates() {
563+
// This test verifies that dragCameraOptions catches exceptions from the native layer
564+
// when given invalid coordinates (infinity) and returns empty CameraOptions instead of crashing.
565+
566+
let fromPoint = CGPoint(x: 0, y: 0)
567+
let toPoint = CGPoint(x: Double.infinity, y: Double.infinity)
568+
569+
let cameraOptions = mapboxMap.dragCameraOptions(from: fromPoint, to: toPoint)
570+
571+
// Should not crash and should return empty CameraOptions (all properties nil)
572+
XCTAssertNotNil(cameraOptions, "dragCameraOptions should not crash with infinite coordinates")
573+
XCTAssertNil(cameraOptions.center, "CameraOptions should be empty after exception")
574+
XCTAssertNil(cameraOptions.zoom, "CameraOptions should be empty after exception")
575+
XCTAssertNil(cameraOptions.bearing, "CameraOptions should be empty after exception")
576+
XCTAssertNil(cameraOptions.pitch, "CameraOptions should be empty after exception")
577+
}
578+
579+
func testCameraForCoordinateBoundsWithNaNCoordinates() {
580+
// Test camera(for coordinateBounds:...) with invalid bounds containing NaN
581+
// Note: The native method handles this gracefully with its own try/catch,
582+
// but we wrap it with NSExceptionHandler.try for additional safety.
583+
584+
let invalidBounds = CoordinateBounds(
585+
southwest: CLLocationCoordinate2D(latitude: Double.nan, longitude: Double.nan),
586+
northeast: CLLocationCoordinate2D(latitude: Double.nan, longitude: Double.nan)
587+
)
588+
589+
let cameraOptions = mapboxMap.camera(
590+
for: invalidBounds,
591+
padding: nil,
592+
bearing: nil,
593+
pitch: nil,
594+
maxZoom: nil,
595+
offset: nil
596+
)
597+
598+
// Should not crash and should return empty CameraOptions
599+
XCTAssertNotNil(cameraOptions)
600+
XCTAssertNil(cameraOptions.center, "CameraOptions should be empty when given invalid bounds")
601+
XCTAssertNil(cameraOptions.zoom, "CameraOptions should be empty when given invalid bounds")
602+
}
603+
604+
func testCameraForCoordinatesWithNaNCoordinates() {
605+
// Test camera(for coordinates:...) with array containing only invalid NaN/infinity coordinates
606+
// Note: The native method handles this gracefully with its own try/catch,
607+
// but we wrap it with NSExceptionHandler.try for additional safety.
608+
609+
let invalidCoordinates = [
610+
CLLocationCoordinate2D(latitude: Double.nan, longitude: Double.nan),
611+
CLLocationCoordinate2D(latitude: Double.infinity, longitude: Double.infinity)
612+
]
613+
614+
let cameraOptions = mapboxMap.camera(
615+
for: invalidCoordinates,
616+
padding: nil,
617+
bearing: nil,
618+
pitch: nil
619+
)
620+
621+
// Should not crash and should return empty CameraOptions
622+
XCTAssertNotNil(cameraOptions)
623+
XCTAssertNil(cameraOptions.center, "CameraOptions should be empty when given invalid coordinates")
624+
XCTAssertNil(cameraOptions.zoom, "CameraOptions should be empty when given invalid coordinates")
625+
}
626+
627+
func testCameraForGeometryWithNaNCoordinates() {
628+
// Test camera(for geometry:...) with geometry containing NaN coordinates
629+
// This method can throw exceptions from the native layer, similar to dragCameraOptions.
630+
631+
let invalidPoint = Point(CLLocationCoordinate2D(latitude: Double.nan, longitude: Double.nan))
632+
let geometry = Geometry.point(invalidPoint)
633+
634+
let cameraOptions = mapboxMap.camera(
635+
for: geometry,
636+
padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10),
637+
bearing: nil,
638+
pitch: nil
639+
)
640+
641+
// Should not crash and should return empty CameraOptions
642+
XCTAssertNotNil(cameraOptions)
643+
XCTAssertNil(cameraOptions.center, "CameraOptions should be empty after exception")
644+
XCTAssertNil(cameraOptions.zoom, "CameraOptions should be empty after exception")
645+
}
542646
}

0 commit comments

Comments
 (0)