diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 8a24ce6..acb8fd4 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -16,7 +16,7 @@ public struct MapView: UIViewControllerRepresentab var gestures = [MapGesture]() var onStyleLoaded: ((MLNStyle) -> Void)? - var onViewPortChanged: ((MapViewPort) -> Void)? + var onViewProxyChanged: ((MapViewProxy) -> Void)? var mapViewContentInset: UIEdgeInsets? @@ -50,7 +50,7 @@ public struct MapView: UIViewControllerRepresentab MapViewCoordinator( parent: self, onGesture: { processGesture($0, $1) }, - onViewPortChanged: { onViewPortChanged?($0) } + onViewProxyChanged: { onViewProxyChanged?($0) } ) } diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index 304f985..e629871 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -20,15 +20,15 @@ public class MapViewCoordinator: NSObject, MLNMapV var onStyleLoaded: ((MLNStyle) -> Void)? var onGesture: (MLNMapView, UIGestureRecognizer) -> Void - var onViewPortChanged: (MapViewPort) -> Void + var onViewProxyChanged: (MapViewProxy) -> Void init(parent: MapView, onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void, - onViewPortChanged: @escaping (MapViewPort) -> Void) + onViewProxyChanged: @escaping (MapViewProxy) -> Void) { self.parent = parent self.onGesture = onGesture - self.onViewPortChanged = onViewPortChanged + self.onViewProxyChanged = onViewProxyChanged } // MARK: Core UIView Functionality @@ -366,7 +366,7 @@ public class MapViewCoordinator: NSObject, MLNMapV public func mapView(_ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated _: Bool) { // TODO: We could put this in regionIsChangingWith if we calculate significant change/debounce. MainActor.assumeIsolated { - updateViewPort(mapView: mapView, reason: reason) + updateViewProxy(mapView: mapView, reason: reason) guard !suppressCameraUpdatePropagation else { return @@ -376,17 +376,13 @@ public class MapViewCoordinator: NSObject, MLNMapV } } - // MARK: MapViewPort + // MARK: MapViewProxy - @MainActor private func updateViewPort(mapView: MLNMapView, reason: MLNCameraChangeReason) { - // Calculate the Raw "ViewPort" - let calculatedViewPort = MapViewPort( - center: mapView.centerCoordinate, - zoom: mapView.zoomLevel, - direction: mapView.direction, - lastReasonForChange: CameraChangeReason(reason) - ) + @MainActor private func updateViewProxy(mapView: MLNMapView, reason: MLNCameraChangeReason) { + // Calculate the Raw "ViewProxy" + let calculatedViewProxy = MapViewProxy(mapView: mapView, + lastReasonForChange: CameraChangeReason(reason)) - onViewPortChanged(calculatedViewPort) + onViewProxyChanged(calculatedViewProxy) } } diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 29a309b..fa5f912 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -130,9 +130,9 @@ public extension MapView { return result } - func onMapViewPortUpdate(_ onViewPortChanged: @escaping (MapViewPort) -> Void) -> Self { + func onMapViewProxyUpdate(_ onViewProxyChanged: @escaping (MapViewProxy) -> Void) -> Self { var result = self - result.onViewPortChanged = onViewPortChanged + result.onViewProxyChanged = onViewProxyChanged return result } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift index 4212e0b..3118931 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift @@ -1,6 +1,7 @@ import Foundation import MapLibre +@MainActor public enum CameraChangeReason: Hashable { case programmatic case resetNorth diff --git a/Sources/MapLibreSwiftUI/Models/MapViewPort.swift b/Sources/MapLibreSwiftUI/Models/MapViewPort.swift deleted file mode 100644 index 30424df..0000000 --- a/Sources/MapLibreSwiftUI/Models/MapViewPort.swift +++ /dev/null @@ -1,50 +0,0 @@ -import CoreLocation -import Foundation - -/// 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 - - /// The reason the view port was changed. - public let lastReasonForChange: CameraChangeReason? - - public init(center: CLLocationCoordinate2D, - zoom: Double, - direction: CLLocationDirection, - lastReasonForChange: CameraChangeReason?) - { - self.center = center - self.zoom = zoom - self.direction = direction - self.lastReasonForChange = lastReasonForChange - } - - public static func zero(zoom: Double = 10) -> MapViewPort { - MapViewPort(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), - zoom: zoom, - direction: 0, - lastReasonForChange: nil) - } -} - -public extension MapViewPort { - /// Generate a basic MapViewCamera that represents the MapViewPort - /// - /// - Returns: The calculated MapViewCamera - func asMapViewCamera() -> MapViewCamera { - .center(center, - zoom: zoom, - direction: direction) - } -} diff --git a/Sources/MapLibreSwiftUI/Models/MapViewProxy.swift b/Sources/MapLibreSwiftUI/Models/MapViewProxy.swift new file mode 100644 index 0000000..3dcd5d0 --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapViewProxy.swift @@ -0,0 +1,73 @@ +import CoreLocation +import Foundation +import MapLibre + +/// A read only representation of the MapView's current View. +/// +/// Provides access to properties and functions of the underlying MLNMapView, +/// but properties only expose their getter, and functions are only available if they +/// do no change the state of the MLNMapView. Writing directly to properties of MLNMapView +/// could clash with swiftui-dsl's state management, which is why modifiying functions +/// and properties are not exposed. +/// +/// You can use `MapView.onMapViewProxyUpdate(_ onViewProxyChanged: @escaping (MapViewProxy) -> Void)` to +/// recieve access to the MapViewProxy. +/// +/// For more information about the properties and functions, see +/// https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre/mlnmapview +@MainActor +public struct MapViewProxy: Hashable, Equatable { + /// The current center coordinate of the MapView + public var centerCoordinate: CLLocationCoordinate2D { + mapView.centerCoordinate + } + + /// The current zoom value of the MapView + public var zoomLevel: Double { + mapView.zoomLevel + } + + /// The current compass direction of the MapView + public var direction: CLLocationDirection { + mapView.direction + } + + public var visibleCoordinateBounds: MLNCoordinateBounds { + mapView.visibleCoordinateBounds + } + + public var mapViewSize: CGSize { + mapView.frame.size + } + + public var contentInset: UIEdgeInsets { + mapView.contentInset + } + + /// The reason the view port was changed. + public let lastReasonForChange: CameraChangeReason? + + private let mapView: MLNMapView + + public func convert(_ coordinate: CLLocationCoordinate2D, toPointTo: UIView?) -> CGPoint { + mapView.convert(coordinate, toPointTo: toPointTo) + } + + public init(mapView: MLNMapView, + lastReasonForChange: CameraChangeReason?) + { + self.mapView = mapView + self.lastReasonForChange = lastReasonForChange + } +} + +public extension MapViewProxy { + /// Generate a basic MapViewCamera that represents the MapView + /// + /// - Returns: The calculated MapViewCamera + func asMapViewCamera() -> MapViewCamera { + .center(centerCoordinate, + zoom: zoomLevel, + direction: direction) + } +} diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index 59fb355..d0d68ee 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -15,7 +15,7 @@ final class MapViewCoordinatorCameraTests: XCTestCase { mapView = MapView(styleURL: URL(string: "https://maplibre.org")!) coordinator = MapView.Coordinator(parent: mapView) { _, _ in // No action - } onViewPortChanged: { _ in + } onViewProxyChanged: { _ in // No action } } diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift index 52eb1f1..ee5b742 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift @@ -2,6 +2,7 @@ import MapLibre import XCTest @testable import MapLibreSwiftUI +@MainActor final class CameraChangeReasonTests: XCTestCase { func testProgrammatic() { let mlnReason: MLNCameraChangeReason = [.programmatic]