From 72f1320369f7e090b9dd99cf6b0308945d35adc2 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 8 Apr 2024 16:32:24 -0700 Subject: [PATCH 1/2] MapViewPort implementation to listen to raw values of 'camera' at any time --- Sources/MapLibreSwiftUI/MapView.swift | 4 +- .../MapLibreSwiftUI/MapViewCoordinator.swift | 40 +++++++++++++---- .../MapLibreSwiftUI/MapViewModifiers.swift | 6 +++ .../MapLibreSwiftUI/Models/MapViewPort.swift | 43 +++++++++++++++++++ .../MapViewCoordinatorCameraTests.swift | 2 + 5 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 Sources/MapLibreSwiftUI/Models/MapViewPort.swift diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 5b10d9e..2167e37 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -11,6 +11,7 @@ public struct MapView: UIViewRepresentable { var gestures = [MapGesture]() var onStyleLoaded: ((MLNStyle) -> Void)? + var onViewPortChanged: ((MapViewPort) -> Void)? public var mapViewContentInset: UIEdgeInsets = .zero public var isLogoViewHidden = false @@ -42,7 +43,8 @@ public struct MapView: UIViewRepresentable { public func makeCoordinator() -> MapViewCoordinator { MapViewCoordinator( parent: self, - onGesture: { processGesture($0, $1) } + onGesture: { processGesture($0, $1) }, + onViewPortChanged: { onViewPortChanged?($0) } ) } diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index 5a23041..34a77cb 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -19,12 +19,15 @@ public class MapViewCoordinator: NSObject { var onStyleLoaded: ((MLNStyle) -> Void)? var onGesture: (MLNMapView, UIGestureRecognizer) -> Void + var onViewPortChanged: (MapViewPort) -> Void init(parent: MapView, - onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void) + onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void, + onViewPortChanged: @escaping (MapViewPort) -> Void) { self.parent = parent self.onGesture = onGesture + self.onViewPortChanged = onViewPortChanged } // MARK: Core UIView Functionality @@ -204,7 +207,9 @@ extension MapViewCoordinator: MLNMapViewDelegate { addLayers(to: mglStyle) onStyleLoaded?(mglStyle) } - + + // MARK: MapViewCamera + @MainActor private func updateParentCamera(mapView: MLNMapView, reason: MLNCameraChangeReason) { // If any of these are a mismatch, we know the camera is no longer following a desired method, so we should // detach and revert to a .centered camera. If any one of these is true, the desired camera state still @@ -222,13 +227,13 @@ extension MapViewCoordinator: MLNMapViewDelegate { case .centered, .rect, .showcase: false } - + guard !isProgrammaticallyTracking else { // Programmatic tracking is still active, we can ignore camera updates until we unset/fail this boolean // check return } - + // Publish the MLNMapView's "raw" camera state to the MapView camera binding. // This path only executes when the map view diverges from the parent state, so this is a "matter of fact" // state propagation. @@ -236,24 +241,43 @@ extension MapViewCoordinator: MLNMapViewDelegate { zoom: mapView.zoomLevel, // TODO: Pitch doesn't really describe current state pitch: .freeWithinRange( - minimum: mapView.minimumPitch, - maximum: mapView.maximumPitch + minimum: mapView.minimumPitch, + maximum: mapView.maximumPitch ), direction: mapView.direction, reason: CameraChangeReason(reason)) snapshotCamera = newCamera parent.camera = newCamera } - + /// The MapView's region has changed with a specific reason. public func mapView(_ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated _: Bool) { + // FIXME: CI complains about MainActor.assumeIsolated being unavailable before iOS 17, despite building on iOS 17.2... This is an epic hack to fix it for now. I can only assume this is an issue with Xcode pre-15.3 + // TODO: We could put this in regionIsChangingWith if we calculate significant change/debounce. + Task { @MainActor in + updateViewPort(mapView: mapView) + } + guard !suppressCameraUpdatePropagation else { return } - + // FIXME: CI complains about MainActor.assumeIsolated being unavailable before iOS 17, despite building on iOS 17.2... This is an epic hack to fix it for now. I can only assume this is an issue with Xcode pre-15.3 Task { @MainActor in updateParentCamera(mapView: mapView, reason: reason) } } + + // MARK: MapViewPort + + @MainActor private func updateViewPort(mapView: MLNMapView) { + // Calculate the Raw "ViewPort" + let calculatedViewPort = MapViewPort( + center: mapView.centerCoordinate, + zoom: mapView.zoomLevel, + direction: mapView.direction + ) + + onViewPortChanged(calculatedViewPort) + } } diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 7f4641a..05b7d3f 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -99,4 +99,10 @@ public extension MapView { return result } + + func onMapViewPortUpdate(_ onViewPortChanged: @escaping (MapViewPort) -> Void) -> Self { + var result = self + result.onViewPortChanged = onViewPortChanged + return result + } } diff --git a/Sources/MapLibreSwiftUI/Models/MapViewPort.swift b/Sources/MapLibreSwiftUI/Models/MapViewPort.swift new file mode 100644 index 0000000..27174f5 --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapViewPort.swift @@ -0,0 +1,43 @@ +import Foundation +import CoreLocation + +/// A representation of the MapView's current ViewPort. +/// +/// This includes similar data to the MapViewCamera, but represents the raw +/// values associated with the MapView. This information could used to prepare +/// a new MapViewCamera on a scene, to cache the camera state, etc. +public struct MapViewPort: Hashable, Equatable { + + /// The current center coordinate of the MapView + public let center: CLLocationCoordinate2D + + /// The current zoom value of the MapView + public let zoom: Double + + /// The current compass direction of the MapView + public let direction: CLLocationDirection + + public init(center: CLLocationCoordinate2D, zoom: Double, direction: CLLocationDirection) { + self.center = center + self.zoom = zoom + self.direction = direction + } + + public static func zero(zoom: Double = 10) -> MapViewPort { + return MapViewPort(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), + zoom: zoom, + direction: 0) + } +} + +extension MapViewPort { + + /// Generate a basic MapViewCamera that represents the MapViewPort + /// + /// - Returns: The calculated MapViewCamera + public func asMapViewCamera() -> MapViewCamera { + return .center(center, + zoom: zoom, + direction: direction) + } +} diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index e181a80..adbab17 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -13,6 +13,8 @@ final class MapViewCoordinatorCameraTests: XCTestCase { mapView = MapView(styleURL: URL(string: "https://maplibre.org")!) coordinator = MapView.Coordinator(parent: mapView) { _, _ in // No action + } onViewPortChanged: { _ in + // No action } } From 42e690713ab6a341edde42c324c4f496ce23b76c Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Mon, 8 Apr 2024 16:34:00 -0700 Subject: [PATCH 2/2] MapViewPort implementation to listen to raw values of 'camera' at any time --- .../MapLibreSwiftUI/MapViewCoordinator.swift | 24 ++++++++-------- .../MapLibreSwiftUI/MapViewModifiers.swift | 2 +- .../MapLibreSwiftUI/Models/MapViewPort.swift | 28 +++++++++---------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index 34a77cb..635d792 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -207,9 +207,9 @@ extension MapViewCoordinator: MLNMapViewDelegate { addLayers(to: mglStyle) onStyleLoaded?(mglStyle) } - + // MARK: MapViewCamera - + @MainActor private func updateParentCamera(mapView: MLNMapView, reason: MLNCameraChangeReason) { // If any of these are a mismatch, we know the camera is no longer following a desired method, so we should // detach and revert to a .centered camera. If any one of these is true, the desired camera state still @@ -227,13 +227,13 @@ extension MapViewCoordinator: MLNMapViewDelegate { case .centered, .rect, .showcase: false } - + guard !isProgrammaticallyTracking else { // Programmatic tracking is still active, we can ignore camera updates until we unset/fail this boolean // check return } - + // Publish the MLNMapView's "raw" camera state to the MapView camera binding. // This path only executes when the map view diverges from the parent state, so this is a "matter of fact" // state propagation. @@ -241,15 +241,15 @@ extension MapViewCoordinator: MLNMapViewDelegate { zoom: mapView.zoomLevel, // TODO: Pitch doesn't really describe current state pitch: .freeWithinRange( - minimum: mapView.minimumPitch, - maximum: mapView.maximumPitch + minimum: mapView.minimumPitch, + maximum: mapView.maximumPitch ), direction: mapView.direction, reason: CameraChangeReason(reason)) snapshotCamera = newCamera parent.camera = newCamera } - + /// The MapView's region has changed with a specific reason. public func mapView(_ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated _: Bool) { // FIXME: CI complains about MainActor.assumeIsolated being unavailable before iOS 17, despite building on iOS 17.2... This is an epic hack to fix it for now. I can only assume this is an issue with Xcode pre-15.3 @@ -257,19 +257,19 @@ extension MapViewCoordinator: MLNMapViewDelegate { Task { @MainActor in updateViewPort(mapView: mapView) } - + guard !suppressCameraUpdatePropagation else { return } - + // FIXME: CI complains about MainActor.assumeIsolated being unavailable before iOS 17, despite building on iOS 17.2... This is an epic hack to fix it for now. I can only assume this is an issue with Xcode pre-15.3 Task { @MainActor in updateParentCamera(mapView: mapView, reason: reason) } } - + // MARK: MapViewPort - + @MainActor private func updateViewPort(mapView: MLNMapView) { // Calculate the Raw "ViewPort" let calculatedViewPort = MapViewPort( @@ -277,7 +277,7 @@ extension MapViewCoordinator: MLNMapViewDelegate { zoom: mapView.zoomLevel, direction: mapView.direction ) - + onViewPortChanged(calculatedViewPort) } } diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 05b7d3f..16bf7ba 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -99,7 +99,7 @@ public extension MapView { return result } - + func onMapViewPortUpdate(_ onViewPortChanged: @escaping (MapViewPort) -> Void) -> Self { var result = self result.onViewPortChanged = onViewPortChanged diff --git a/Sources/MapLibreSwiftUI/Models/MapViewPort.swift b/Sources/MapLibreSwiftUI/Models/MapViewPort.swift index 27174f5..2fee8b1 100644 --- a/Sources/MapLibreSwiftUI/Models/MapViewPort.swift +++ b/Sources/MapLibreSwiftUI/Models/MapViewPort.swift @@ -1,5 +1,5 @@ -import Foundation import CoreLocation +import Foundation /// A representation of the MapView's current ViewPort. /// @@ -7,37 +7,35 @@ import CoreLocation /// values associated with the MapView. This information could used to prepare /// a new MapViewCamera on a scene, to cache the camera state, etc. public struct MapViewPort: Hashable, Equatable { - /// The current center coordinate of the MapView public let center: CLLocationCoordinate2D - + /// The current zoom value of the MapView public let zoom: Double - + /// The current compass direction of the MapView public let direction: CLLocationDirection - + public init(center: CLLocationCoordinate2D, zoom: Double, direction: CLLocationDirection) { self.center = center self.zoom = zoom self.direction = direction } - + public static func zero(zoom: Double = 10) -> MapViewPort { - return MapViewPort(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), - zoom: zoom, - direction: 0) + MapViewPort(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), + zoom: zoom, + direction: 0) } } -extension MapViewPort { - +public extension MapViewPort { /// Generate a basic MapViewCamera that represents the MapViewPort /// /// - Returns: The calculated MapViewCamera - public func asMapViewCamera() -> MapViewCamera { - return .center(center, - zoom: zoom, - direction: direction) + func asMapViewCamera() -> MapViewCamera { + .center(center, + zoom: zoom, + direction: direction) } }