diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftUI-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftUI-Package.xcscheme
index 6ccb3d7..afee237 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftUI-Package.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftUI-Package.xcscheme
@@ -77,6 +77,16 @@
ReferencedContainer = "container:">
+
+
+
+
[StyleLayerDefinition] {
- return layers.flatMap { $0 }
- }
-
- public static func buildOptional(_ layers: [StyleLayerDefinition]?) -> [StyleLayerDefinition] {
- return layers ?? []
+public enum MapViewContentBuilder: DefaultResultBuilder {
+ public static func buildExpression(_ expression: StyleLayerDefinition) -> [StyleLayerDefinition] {
+ return [expression]
}
- public static func buildExpression(_ layer: StyleLayerDefinition) -> [StyleLayerDefinition] {
- return [layer]
+ public static func buildExpression(_ expression: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
+ return expression
}
public static func buildExpression(_ expression: Void) -> [StyleLayerDefinition] {
return []
}
- public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] {
- return styleCollection.layers
+ public static func buildBlock(_ components: [StyleLayerDefinition]...) -> [StyleLayerDefinition] {
+ return components.flatMap { $0 }
+ }
+
+ public static func buildArray(_ components: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
+ return components
+ }
+
+ public static func buildArray(_ components: [[StyleLayerDefinition]]) -> [StyleLayerDefinition] {
+ return components.flatMap { $0 }
}
- // Handle an array of MLNShape (if you want to directly pass arrays)
- public static func buildArray(_ layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
- return layer
+ public static func buildEither(first components: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
+ return components
}
- // Handle for in of MLNShape
- public static func buildArray(_ layer: [[StyleLayerDefinition]]) -> [StyleLayerDefinition] {
- return layer.flatMap { $0 }
+ public static func buildEither(second components: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
+ return components
}
- public static func buildEither(first layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
- return layer
+ public static func buildOptional(_ components: [StyleLayerDefinition]?) -> [StyleLayerDefinition] {
+ return components ?? []
}
- public static func buildEither(second layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
- return layer
+ // MARK: Custom Handler for StyleLayerCollection type.
+
+ public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] {
+ return styleCollection.layers
}
}
diff --git a/Sources/MapLibreSwiftDSL/Data Sources.swift b/Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift
similarity index 92%
rename from Sources/MapLibreSwiftDSL/Data Sources.swift
rename to Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift
index 988af55..1b64c04 100644
--- a/Sources/MapLibreSwiftDSL/Data Sources.swift
+++ b/Sources/MapLibreSwiftDSL/ShapeDataBuilder.swift
@@ -45,8 +45,7 @@ public struct ShapeSource: Source {
@resultBuilder
-public enum ShapeDataBuilder {
- // Handle a single MLNShape element
+public enum ShapeDataBuilder: DefaultResultBuilder {
public static func buildExpression(_ expression: MLNShape) -> [MLNShape] {
return [expression]
}
@@ -55,22 +54,22 @@ public enum ShapeDataBuilder {
return expression
}
- // Combine elements into an array
+ public static func buildExpression(_ expression: Void) -> [MLNShape] {
+ return []
+ }
+
public static func buildBlock(_ components: [MLNShape]...) -> [MLNShape] {
return components.flatMap { $0 }
}
- // Handle an array of MLNShape (if you want to directly pass arrays)
public static func buildArray(_ components: [MLNShape]) -> [MLNShape] {
return components
}
- // Handle for in of MLNShape
public static func buildArray(_ components: [[MLNShape]]) -> [MLNShape] {
return components.flatMap { $0 }
}
- // Handle if statements
public static func buildEither(first components: [MLNShape]) -> [MLNShape] {
return components
}
diff --git a/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift b/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift
new file mode 100644
index 0000000..43aa44f
--- /dev/null
+++ b/Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+/// Enforces a basic set of result builder definiitons.
+///
+/// This is just a tool to make a result builder easier to build, maintain sorting, etc.
+public protocol DefaultResultBuilder {
+
+ associatedtype Component
+
+ static func buildExpression(_ expression: Component) -> [Component]
+
+ static func buildExpression(_ expression: [Component]) -> [Component]
+
+ // MARK: Handle void
+
+ static func buildExpression(_ expression: Void) -> [Component]
+
+ // MARK: Combine elements into an array
+
+ static func buildBlock(_ components: [Component]...) -> [Component]
+
+ // MARK: Handle Arrays
+
+ static func buildArray(_ components: [Component]) -> [Component]
+
+ // MARK: Handle for in loops
+
+ static func buildArray(_ components: [[Component]]) -> [Component]
+
+ // MARK: Handle if statements
+
+ static func buildEither(first components: [Component]) -> [Component]
+
+ static func buildEither(second components: [Component]) -> [Component]
+
+ // MARK: Handle Optionals
+
+ static func buildOptional(_ components: [Component]?) -> [Component]
+}
diff --git a/Sources/MapLibreSwiftUI/Examples/Camera.swift b/Sources/MapLibreSwiftUI/Examples/Camera.swift
index 3ab37ff..5bfec4e 100644
--- a/Sources/MapLibreSwiftUI/Examples/Camera.swift
+++ b/Sources/MapLibreSwiftUI/Examples/Camera.swift
@@ -11,7 +11,7 @@ struct CameraDirectManipulationPreview: View {
var body: some View {
MapView(styleURL: styleURL, camera: $camera)
.overlay(alignment: .bottom, content: {
- Text("\(camera.coordinate.latitude), \(camera.coordinate.longitude) z \(camera.zoom)")
+ Text("\(String(describing: camera.state)) z \(camera.zoom)")
.padding()
.foregroundColor(.white)
.background(
diff --git a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift
index a40afbf..6230bfb 100644
--- a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift
+++ b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNCameraChangeReason.swift
@@ -1,23 +1,7 @@
import Foundation
import MapLibre
-extension MLNCameraChangeReason: CustomDebugStringConvertible {
- public var debugDescription: String {
- switch self.lastValue {
-
- case .programmatic: return ".programmatic"
- case .resetNorth: return ".resetNorth"
- case .gesturePan: return ".gesturePan"
- case .gesturePinch: return ".gesturePinch"
- case .gestureRotate: return ".gestureRotate"
- case .gestureZoomIn: return ".gestureZoomIn"
- case .gestureZoomOut: return ".gestureZoomOut"
- case .gestureOneFingerZoom: return ".gestureOneFingerZoom"
- case .gestureTilt: return ".gestureTilt"
- case .transitionCancelled: return ".transitionCancelled"
- default: return "none"
- }
- }
+extension MLNCameraChangeReason {
/// Get the last value from the MLNCameraChangeReason option set.
public var lastValue: MLNCameraChangeReason {
diff --git a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift
new file mode 100644
index 0000000..f113599
--- /dev/null
+++ b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift
@@ -0,0 +1,64 @@
+import Foundation
+import MapLibre
+
+extension MapView {
+
+ /// Register a gesture recognizer on the MapView.
+ ///
+ /// - Parameters:
+ /// - mapView: The MLNMapView that will host the gesture itself.
+ /// - context: The UIViewRepresentable context that will orchestrate the response sender
+ /// - gesture: The gesture definition.
+ func registerGesture(_ mapView: MLNMapView, _ context: Context, gesture: MapGesture) {
+ switch gesture.method {
+
+ case .tap(numberOfTaps: let numberOfTaps):
+ let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator,
+ action: #selector(context.coordinator.captureGesture(_:)))
+ gestureRecognizer.numberOfTapsRequired = numberOfTaps
+ mapView.addGestureRecognizer(gestureRecognizer)
+ gesture.gestureRecognizer = gestureRecognizer
+
+ case .longPress(minimumDuration: let minimumDuration):
+ let gestureRecognizer = UILongPressGestureRecognizer(target: context.coordinator,
+ action: #selector(context.coordinator.captureGesture(_:)))
+ gestureRecognizer.minimumPressDuration = minimumDuration
+
+ mapView.addGestureRecognizer(gestureRecognizer)
+ gesture.gestureRecognizer = gestureRecognizer
+ }
+ }
+
+ /// Runs on each gesture change event and filters the appropriate gesture behavior based on the
+ /// user definition.
+ ///
+ /// Since the gestures run "onChange", we run this every time, event when state changes. The implementer is responsible for guarding
+ /// and handling whatever state logic they want.
+ ///
+ /// - Parameters:
+ /// - mapView: The MapView emitting the gesture. This is used to calculate the point and coordinate of the gesture.
+ /// - sender: The UIGestureRecognizer
+ func processGesture(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) {
+ guard let gesture = self.gestures.first(where: { $0.gestureRecognizer == sender }) else {
+ assertionFailure("\(sender) is not a registered UIGestureRecongizer on the MapView")
+ return
+ }
+
+ // Build the context of the gesture's event.
+ var point: CGPoint
+ switch gesture.method {
+
+ case .tap(numberOfTaps: let numberOfTaps):
+ point = sender.location(ofTouch: numberOfTaps - 1, in: mapView)
+ case .longPress:
+ point = sender.location(in: mapView)
+ }
+
+ let context = MapGestureContext(gestureMethod: gesture.method,
+ state: sender.state,
+ point: point,
+ coordinate: mapView.convert(point, toCoordinateFrom: mapView))
+
+ gesture.onChange(context)
+ }
+}
diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift
index 6e792b4..da9d840 100644
--- a/Sources/MapLibreSwiftUI/MapView.swift
+++ b/Sources/MapLibreSwiftUI/MapView.swift
@@ -38,7 +38,7 @@ public struct MapView: UIViewRepresentable {
public func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(
parent: self,
- onGestureEnd: { processGestureEnd($0, $1) }
+ onGesture: { processGesture($0, $1) }
)
}
@@ -60,19 +60,11 @@ public struct MapView: UIViewRepresentable {
// TODO: Make this settable via a modifier
mapView.logoView.isHidden = true
- // Gesture recogniser setup
- let tapGesture = UITapGestureRecognizer(
- target: context.coordinator,
- action: #selector(context.coordinator.captureGesture(_:))
- )
- mapView.addGestureRecognizer(tapGesture)
-
- let longPressGesture = UILongPressGestureRecognizer(
- target: context.coordinator,
- action: #selector(context.coordinator.captureGesture(_:))
- )
- mapView.addGestureRecognizer(longPressGesture)
-
+ // Add all gesture recognizers
+ for gesture in gestures {
+ registerGesture(mapView, context, gesture: gesture)
+ }
+
return mapView
}
@@ -95,45 +87,6 @@ public struct MapView: UIViewRepresentable {
camera: $camera.wrappedValue,
animated: isStyleLoaded)
}
-
- /// Runs on gesture ended.
- ///
- /// Note: Some gestures may need additional behaviors for different gesture.states.
- ///
- /// - Parameters:
- /// - mapView: The MapView emitting the gesture. This is used to calculate the point and coordinate of the gesture.
- /// - sender: The UIGestureRecognizer
- private func processGestureEnd(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) {
- guard sender.state == .ended else {
- return
- }
-
- let point = sender.location(in: mapView)
- let coordinate = mapView.convert(point, toCoordinateFrom: mapView)
-
- switch sender {
- case is UITapGestureRecognizer:
- for gesture in gestures.filter({ $0.method == .tap }) {
- gesture.action(
- MapGestureContext(gesture: gesture.method,
- point: point,
- coordinate: coordinate,
- numberOfTaps: sender.numberOfTouches)
- )
- }
- case is UILongPressGestureRecognizer:
- for gesture in gestures.filter({ $0.method == .longPress }) {
- gesture.action(
- MapGestureContext(gesture: gesture.method,
- point: point,
- coordinate: coordinate,
- numberOfTaps: sender.numberOfTouches)
- )
- }
- default:
- print("Log unhandled gesture")
- }
- }
}
struct MapView_Previews: PreviewProvider {
diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift
index 330595d..c96f894 100644
--- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift
+++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift
@@ -12,22 +12,22 @@ public class MapViewCoordinator: NSObject {
// every update cycle so we can avoid unnecessary updates
private var snapshotUserLayers: [StyleLayerDefinition] = []
private var snapshotCamera: MapViewCamera?
- private var onGestureEnd: (MLNMapView, UIGestureRecognizer) -> Void
+ private var onGesture: (MLNMapView, UIGestureRecognizer) -> Void
init(parent: MapView,
- onGestureEnd: @escaping (MLNMapView, UIGestureRecognizer) -> Void) {
+ onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void) {
self.parent = parent
- self.onGestureEnd = onGestureEnd
+ self.onGesture = onGesture
}
// MARK: Core UIView Functionality
@objc func captureGesture(_ sender: UIGestureRecognizer) {
- guard let mapView, sender.state == .ended else {
+ guard let mapView else {
return
}
- onGestureEnd(mapView, sender)
+ onGesture(mapView, sender)
}
// MARK: - Coordinator API - Camera + Manipulation
@@ -39,11 +39,11 @@ public class MapViewCoordinator: NSObject {
}
switch camera.state {
- case .centered:
+ case .centered(let coordinate):
mapView.userTrackingMode = .none
- mapView.setCenter(camera.coordinate,
+ mapView.setCenter(coordinate,
zoomLevel: camera.zoom,
- direction: camera.course,
+ direction: camera.direction,
animated: animated)
case .trackingUserLocation:
mapView.userTrackingMode = .follow
@@ -59,13 +59,9 @@ public class MapViewCoordinator: NSObject {
break
}
- if let pitch = camera.pitch {
- mapView.minimumPitch = pitch
- mapView.maximumPitch = pitch
- } else {
- mapView.minimumPitch = 0
- mapView.maximumPitch = 90
- }
+ // Set the correct pitch range.
+ mapView.minimumPitch = camera.pitch.rangeValue.lowerBound
+ mapView.maximumPitch = camera.pitch.rangeValue.upperBound
snapshotCamera = camera
}
diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift
index b2ce161..d1ef2f3 100644
--- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift
+++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift
@@ -6,6 +6,7 @@ import SwiftUI
import MapLibre
extension MapView {
+
/// Allows you to set properties of the underlying MLNMapView directly
/// in cases where these have not been ported to DSL yet.
/// Use this function to modify various properties of the MLNMapView instance.
@@ -38,21 +39,31 @@ extension MapView {
// MARK: Default Gestures
- public func onTapMapGesture(onTapChanged: @escaping (MapGestureContext) -> Void) -> MapView {
+ /// Add an onTap gesture to the MapView
+ ///
+ /// - Parameters:
+ /// - count: The number of taps required to run the gesture.
+ /// - onTapChanged: <#onTapChanged description#>
+ /// - Returns: <#description#>
+ public func onTapMapGesture(count: Int = 1,
+ onTapChanged: @escaping (MapGestureContext) -> Void) -> MapView {
var newMapView = self
// Build the gesture and link it to the map view.
- let gesture = MapGesture(method: .tap, action: onTapChanged)
+ let gesture = MapGesture(method: .tap(numberOfTaps: count),
+ onChange: onTapChanged)
newMapView.gestures.append(gesture)
return newMapView
}
- public func onLongPressMapGesture(onPressChanged: @escaping (MapGestureContext) -> Void) -> MapView {
+ public func onLongPressMapGesture(minimumDuration: Double = 0.5,
+ onPressChanged: @escaping (MapGestureContext) -> Void) -> MapView {
var newMapView = self
// Build the gesture and link it to the map view.
- let gesture = MapGesture(method: .longPress, action: onPressChanged)
+ let gesture = MapGesture(method: .longPress(minimumDuration: minimumDuration),
+ onChange: onPressChanged)
newMapView.gestures.append(gesture)
return newMapView
diff --git a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift
index 16f8ead..3c74e76 100644
--- a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift
+++ b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift
@@ -1,19 +1,38 @@
-import Foundation
+import UIKit
-public struct MapGesture {
+public class MapGesture: NSObject {
public enum Method: Equatable {
/// A standard tap gesture (UITapGestureRecognizer)
- case tap
+ ///
+ /// - Parameters:
+ /// - numberOfTaps: The number of taps required for the gesture to trigger
+ case tap(numberOfTaps: Int = 1)
/// A standard long press gesture (UILongPressGestureRecognizer)
- case longPress
+ ///
+ /// - Parameters:
+ /// - minimumDuration: The minimum duration of the press in seconds.
+ case longPress(minimumDuration: Double)
}
/// The Gesture's method, this is used to register it for the correct user interaction on the MapView.
let method: Method
- /// The action that runs when the gesture is triggered from the map view.
- let action: (MapGestureContext) -> Void
+ /// The onChange action that runs when the gesture changes on the map view.
+ let onChange: (MapGestureContext) -> Void
+
+ /// The underlying gesture recognizer
+ var gestureRecognizer: UIGestureRecognizer?
+
+ /// Create a new gesture recognizer definition for the MapView
+ ///
+ /// - Parameters:
+ /// - method: The gesture recognizer method
+ /// - onChange: The action to perform when the gesture is changed
+ init(method: Method, onChange: @escaping (MapGestureContext) -> Void) {
+ self.method = method
+ self.onChange = onChange
+ }
}
diff --git a/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift b/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift
index e2f67ee..0394e1b 100644
--- a/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift
+++ b/Sources/MapLibreSwiftUI/Models/Gesture/MapGestureContext.swift
@@ -1,17 +1,18 @@
-import Foundation
+import UIKit
import CoreLocation
+/// The contextual representation of the gesture.
public struct MapGestureContext {
-
+
/// The map gesture that produced the context.
- public let gesture: MapGesture.Method
+ public let gestureMethod: MapGesture.Method
+
+ /// The state of the on change event.
+ public let state: UIGestureRecognizer.State
/// The location that the gesture occured on the screen.
public let point: CGPoint
/// The underlying geographic coordinate at the point of the gesture.
public let coordinate: CLLocationCoordinate2D
-
- /// The number of taps (of a tap gesture)
- public let numberOfTaps: Int?
}
diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift
index c732efe..adaf325 100644
--- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift
+++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraChangeReason.swift
@@ -1,7 +1,7 @@
import Foundation
import MapLibre
-public enum CameraChangeReason: Hashable, Codable {
+public enum CameraChangeReason: Hashable {
case programmatic
case resetNorth
case gesturePan
diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift
new file mode 100644
index 0000000..a6c7492
--- /dev/null
+++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraPitch.swift
@@ -0,0 +1,30 @@
+import Foundation
+import MapLibre
+
+/// The current pitch state for the MapViewCamera
+public enum CameraPitch: Hashable {
+
+ /// The user is free to control pitch from it's default min to max.
+ case free
+
+ /// The user is free to control pitch within the minimum and maximum range.
+ case withinRange(minimum: Double, maximum: Double)
+
+ /// The pitch is fixed to a certain value.
+ case fixed(Double)
+
+ /// The range of acceptable pitch values.
+ ///
+ /// This is applied to the map view on camera updates.
+ var rangeValue: ClosedRange {
+ switch self {
+
+ case .free:
+ return 0...60 // TODO: set this to a maplibre constant (this is available on Android, but maybe not iOS)?
+ case .withinRange(minimum: let minimum, maximum: let maximum):
+ return minimum...maximum
+ case .fixed(let value):
+ return value...value
+ }
+ }
+}
diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift
index 5370971..977c914 100644
--- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift
+++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift
@@ -2,23 +2,49 @@ import Foundation
import MapLibre
/// The CameraState is used to understand the current context of the MapView's camera.
-public enum CameraState: Hashable, Codable {
+public enum CameraState: Hashable {
/// Centered on a coordinate
- case centered
+ case centered(onCenter: CLLocationCoordinate2D)
/// Follow the user's location using the MapView's internal camera.
case trackingUserLocation
/// Follow the user's location using the MapView's internal camera with the user's heading.
+ ///
+ /// This feature uses the MLNMapView's userTrackingMode to .followWithHeading which automatically
+ /// follows the user from within the MLNMapView.
case trackingUserLocationWithHeading
/// Follow the user's location using the MapView's internal camera with the users' course
+ ///
+ /// This feature uses the MLNMapView's userTrackingMode to .followWithCourse which automatically
+ /// follows the user from within the MLNMapView.
case trackingUserLocationWithCourse
/// Centered on a bounding box/rectangle.
- case rect
+ case rect(northeast: CLLocationCoordinate2D, southwest: CLLocationCoordinate2D) // TODO: make a bounding box?
- /// Showcasing a GeoJSON/Polygon
- case showcase
+ /// Showcasing GeoJSON, Polygons, etc.
+ case showcase(shapeCollection: MLNShapeCollection)
+}
+
+extension CameraState: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ switch self {
+
+ case .centered(onCenter: let onCenter):
+ return ".center(onCenter: \(onCenter)"
+ case .trackingUserLocation:
+ return ".trackingUserLocation"
+ case .trackingUserLocationWithHeading:
+ return ".trackingUserLocationWithHeading"
+ case .trackingUserLocationWithCourse:
+ return ".trackingUserLocationWithCourse"
+ case .rect(northeast: let northeast, southwest: let southwest):
+ return ".rect(northeast: \(northeast), southwest: \(southwest))"
+ case .showcase(shapeCollection: let shapeCollection):
+ return ".showcase(shapeCollection: \(shapeCollection))"
+ }
+ }
}
diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift
index 505bd6d..7196df6 100644
--- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift
+++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift
@@ -2,20 +2,22 @@ import Foundation
import CoreLocation
import MapLibre
+/// The SwiftUI MapViewCamera.
+///
+/// This manages the camera state within the MapView.
public struct MapViewCamera: Hashable {
public struct Defaults {
public static let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
public static let zoom: Double = 10
- public static let pitch: Double? = nil
- public static let course: Double = 0
+ public static let pitch: CameraPitch = .free
+ public static let direction: CLLocationDirection = 0
}
public var state: CameraState
- public var coordinate: CLLocationCoordinate2D
public var zoom: Double
- public var pitch: Double?
- public var course: CLLocationDirection
+ public var pitch: CameraPitch
+ public var direction: CLLocationDirection
/// The reason the camera was changed.
///
@@ -29,11 +31,10 @@ public struct MapViewCamera: Hashable {
///
/// - Returns: The constructed MapViewCamera.
public static func `default`() -> MapViewCamera {
- return MapViewCamera(state: .centered,
- coordinate: Defaults.coordinate,
+ return MapViewCamera(state: .centered(onCenter: Defaults.coordinate),
zoom: Defaults.zoom,
pitch: Defaults.pitch,
- course: Defaults.course,
+ direction: Defaults.direction,
lastReasonForChange: .programmatic)
}
@@ -43,19 +44,18 @@ public struct MapViewCamera: Hashable {
/// - coordinate: The coordinate to center the map on.
/// - zoom: The zoom level.
/// - pitch: The camera pitch. Default is 90 (straight down).
- /// - course: The course. Default is 0 (North).
+ /// - direction: The course. Default is 0 (North).
/// - Returns: The constructed MapViewCamera.
public static func center(_ coordinate: CLLocationCoordinate2D,
zoom: Double,
- pitch: Double? = Defaults.pitch,
- course: Double = Defaults.course,
+ pitch: CameraPitch = Defaults.pitch,
+ direction: CLLocationDirection = Defaults.direction,
reason: CameraChangeReason? = nil) -> MapViewCamera {
- return MapViewCamera(state: .centered,
- coordinate: coordinate,
+ return MapViewCamera(state: .centered(onCenter: coordinate),
zoom: zoom,
pitch: pitch,
- course: course,
+ direction: direction,
lastReasonForChange: reason)
}
@@ -68,69 +68,53 @@ public struct MapViewCamera: Hashable {
/// - pitch: Provide a fixed pitch value. The user will not be able to adjust pitch using gestures when this is set. Use nil/default to allow user control.
/// - Returns: The MapViewCamera representing the scenario
public static func trackUserLocation(zoom: Double = Defaults.zoom,
- pitch: Double? = Defaults.pitch) -> MapViewCamera {
+ pitch: CameraPitch = Defaults.pitch) -> MapViewCamera {
// Coordinate is ignored when tracking user location. However, pitch and zoom are valid.
return MapViewCamera(state: .trackingUserLocation,
- coordinate: Defaults.coordinate,
zoom: zoom,
pitch: pitch,
- course: Defaults.course,
+ direction: Defaults.direction,
lastReasonForChange: .programmatic)
}
/// Enables user location tracking within the MapView.
///
- /// This feature uses the MLNMapView's userTrackingMode = .follow
+ /// This feature uses the MLNMapView's userTrackingMode = .followWithHeading
///
/// - Parameters:
/// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike pitch.
/// - pitch: Provide a fixed pitch value. The user will not be able to adjust pitch using gestures when this is set. Use nil/default to allow user control.
/// - Returns: The MapViewCamera representing the scenario
public static func trackUserLocationWithHeading(zoom: Double = Defaults.zoom,
- pitch: Double? = Defaults.pitch) -> MapViewCamera {
+ pitch: CameraPitch = Defaults.pitch) -> MapViewCamera {
// Coordinate is ignored when tracking user location. However, pitch and zoom are valid.
return MapViewCamera(state: .trackingUserLocationWithHeading,
- coordinate: Defaults.coordinate,
zoom: zoom,
pitch: pitch,
- course: Defaults.course,
+ direction: Defaults.direction,
lastReasonForChange: .programmatic)
}
/// Enables user location tracking within the MapView.
///
- /// This feature uses the MLNMapView's userTrackingMode = .follow
+ /// This feature uses the MLNMapView's userTrackingMode = .followWithCourse
///
/// - Parameters:
/// - zoom: Set the desired zoom. This is a one time event and the user can manipulate their zoom after unlike pitch.
/// - pitch: Provide a fixed pitch value. The user will not be able to adjust pitch using gestures when this is set. Use nil/default to allow user control.
/// - Returns: The MapViewCamera representing the scenario
public static func trackUserLocationWithCourse(zoom: Double = Defaults.zoom,
- pitch: Double? = Defaults.pitch) -> MapViewCamera {
+ pitch: CameraPitch = Defaults.pitch) -> MapViewCamera {
// Coordinate is ignored when tracking user location. However, pitch and zoom are valid.
return MapViewCamera(state: .trackingUserLocationWithCourse,
- coordinate: Defaults.coordinate,
zoom: zoom,
pitch: pitch,
- course: Defaults.course,
+ direction: Defaults.direction,
lastReasonForChange: .programmatic)
}
// TODO: Create init methods for other camera states once supporting materials are understood (e.g. BoundingBox)
}
-
-extension MapViewCamera: Equatable {
-
- public static func ==(lhs: MapViewCamera, rhs: MapViewCamera) -> Bool {
- return lhs.state == rhs.state
- && lhs.coordinate.latitude == rhs.coordinate.latitude
- && lhs.coordinate.longitude == rhs.coordinate.longitude
- && lhs.zoom == rhs.zoom
- && lhs.pitch == rhs.pitch
- && lhs.course == rhs.course
- && lhs.lastReasonForChange == rhs.lastReasonForChange
- }
-}
diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift
new file mode 100644
index 0000000..c197060
--- /dev/null
+++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraChangeReasonTests.swift
@@ -0,0 +1,12 @@
+import XCTest
+import MapLibre
+@testable import MapLibreSwiftUI
+
+final class CameraChangeReasonTests: XCTestCase {
+
+ func testGestureOneFingerZoom() {
+ let mlnReason: MLNCameraChangeReason = [.programmatic, .gestureOneFingerZoom]
+ XCTAssertEqual(CameraChangeReason(mlnReason), .gestureOneFingerZoom)
+ }
+
+}
diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift
new file mode 100644
index 0000000..71d1b2b
--- /dev/null
+++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraPitchTests.swift
@@ -0,0 +1,23 @@
+import XCTest
+@testable import MapLibreSwiftUI
+
+final class CameraPitchTests: XCTestCase {
+
+ func testFreePitch() {
+ let pitch: CameraPitch = .free
+ XCTAssertEqual(pitch.rangeValue.lowerBound, 0)
+ XCTAssertEqual(pitch.rangeValue.upperBound, 60)
+ }
+
+ func testRangePitch() {
+ let pitch = CameraPitch.withinRange(minimum: 9, maximum: 29)
+ XCTAssertEqual(pitch.rangeValue.lowerBound, 9)
+ XCTAssertEqual(pitch.rangeValue.upperBound, 29)
+ }
+
+ func testFixedPitch() {
+ let pitch = CameraPitch.fixed(41)
+ XCTAssertEqual(pitch.rangeValue.lowerBound, 41)
+ XCTAssertEqual(pitch.rangeValue.upperBound, 41)
+ }
+}
diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift
new file mode 100644
index 0000000..4b44676
--- /dev/null
+++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/CameraStateTests.swift
@@ -0,0 +1,36 @@
+import XCTest
+import CoreLocation
+@testable import MapLibreSwiftUI
+
+final class CameraStateTests: XCTestCase {
+
+ func testCenterCameraState() {
+ let expectedCoordinate = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4)
+ let state: CameraState = .centered(onCenter: expectedCoordinate)
+ XCTAssertEqual(state, .centered(onCenter: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4)))
+ }
+
+ func testTrackingUserLocation() {
+ let state: CameraState = .trackingUserLocation
+ XCTAssertEqual(state, .trackingUserLocation)
+ }
+
+ func testTrackingUserLocationWithHeading() {
+ let state: CameraState = .trackingUserLocationWithHeading
+ XCTAssertEqual(state, .trackingUserLocationWithHeading)
+ }
+
+ func testTrackingUserLocationWithCourse() {
+ let state: CameraState = .trackingUserLocationWithCourse
+ XCTAssertEqual(state, .trackingUserLocationWithCourse)
+ }
+
+ func testRect() {
+ let northeast = CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4)
+ let southwest = CLLocationCoordinate2D(latitude: 34.5, longitude: 45.6)
+
+ let state: CameraState = .rect(northeast: northeast, southwest: southwest)
+ XCTAssertEqual(state, .rect(northeast: northeast, southwest: southwest))
+ }
+
+}
diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift
new file mode 100644
index 0000000..6e97796
--- /dev/null
+++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/MapViewCameraTests.swift
@@ -0,0 +1,6 @@
+import XCTest
+@testable import MapLibreSwiftUI
+
+final class MapViewCameraTests: XCTestCase {
+
+}