From 99504f6017bb0823b7f028934a49af6b03c597da Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Sat, 16 Dec 2023 17:14:19 -0800 Subject: [PATCH 1/4] Added map camera, layer mods, and more --- .../xcschemes/MapLibreSwiftDSL.xcscheme | 2 +- .../MapLibreSwiftUI-Package.xcscheme | 2 +- .../xcschemes/MapLibreSwiftUI.xcscheme | 2 +- Package.swift | 4 +- Sources/MapLibreSwiftUI/Examples/Camera.swift | 29 ++++---- .../MapLibreSwiftUI/Examples/Polyline.swift | 2 +- Sources/MapLibreSwiftUI/MapView.swift | 53 +++++++------- .../Models/MapCamera/CameraState.swift | 44 ++++++++++++ .../Models/MapCamera/MapViewCamera.swift | 69 +++++++++++++++++++ .../MapStyleSource/MapStyleSource.swift | 13 ++++ .../MapViewContentViewModifier.swift | 22 ++++++ 11 files changed, 195 insertions(+), 47 deletions(-) create mode 100644 Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift create mode 100644 Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift create mode 100644 Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift create mode 100644 Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme index 0bdca09..a45b7e5 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/MapLibreSwiftDSL.xcscheme @@ -1,6 +1,6 @@ ? - var camera: Binding? - - let styleSource: MapStyleSource + public let styleSource: MapStyleSource let userLayers: [StyleLayerDefinition] - public init(styleURL: URL, camera: Binding? = nil, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }) { + public init( + styleURL: URL, + camera: Binding? = nil, + @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } + ) { self.styleSource = .url(styleURL) self.camera = camera userLayers = makeMapContent() } - public init(styleURL: URL, initialCamera: Camera, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }) { + public init( + styleURL: URL, + initialCamera: MapViewCamera, + @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } + ) { self.init(styleURL: styleURL, camera: .constant(initialCamera), makeMapContent) } @@ -58,7 +59,8 @@ public struct MapView: UIViewRepresentable { public func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) { DispatchQueue.main.async { - self.parent.camera?.wrappedValue = .centerAndZoom(mapView.centerCoordinate, mapView.zoomLevel) + self.parent.camera?.wrappedValue = .center(mapView.centerCoordinate, + zoom: mapView.zoomLevel) } } @@ -186,17 +188,18 @@ public struct MapView: UIViewRepresentable { } private func updateMapCamera(_ mapView: MLNMapView, animated: Bool) { - if let camera = self.camera { - switch camera.wrappedValue { - case .centerAndZoom(let center, let zoom): - // TODO: Determine if MapLibre is smart enough to keep animating to the same place multiple times; if not, add a check here to prevent suprious updates. - if let z = zoom { - mapView.setCenter(center, zoomLevel: z, animated: animated) - } else { - mapView.setCenter(center, animated: animated) - } - } + guard let newCamera = self.camera?.wrappedValue, + lastCamera != newCamera else { + // Exit early - the camera has not changed. + return } + + mapView.setCenter(newCamera.coordinate, + zoomLevel: newCamera.zoom, + direction: newCamera.course, + animated: animated) + + self.lastCamera = newCamera } } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift new file mode 100644 index 0000000..f3bebac --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -0,0 +1,44 @@ +// +// File.swift +// +// +// Created by Jacob Fielding on 12/16/23. +// + +import Foundation +import MapLibre + +/// The CameraState is used to understand the current context of the MapView's camera. +public enum CameraState { + + /// Centered on a coordinate + case centered + + /// The camera is currently following a location provider. + case userLocation + + /// Centered on a bounding box/rectangle. + case rect + + /// Showcasing a GeoJSON/Polygon + case showcase +} + +extension CameraState: Equatable { + + public static func ==(lhs: CameraState, rhs: CameraState) -> Bool { + switch (lhs, rhs) { + + case (.centered, .centered): + return true + case (.userLocation, .userLocation): + return true + case (.rect, .rect): + return true + case (.showcase, .showcase): + return true + default: + return false + } + } +} diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift new file mode 100644 index 0000000..8e977da --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -0,0 +1,69 @@ +import Foundation +import CoreLocation + +public struct MapViewCamera { + + public var state: CameraState + public var coordinate: CLLocationCoordinate2D + public var zoom: Double + public var pitch: Double + public var course: CLLocationDirection + + /// A backup camera centered at 0.0, 0.0. This is typically used as a backup, + /// pre-load for an expected camera update (e.g. before a location provider produces + /// it's first location). + /// + /// - Returns: The constructed MapViewCamera. + public static func backup() -> MapViewCamera { + return MapViewCamera(state: .centered, + coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0), + zoom: 10, + pitch: 90, + course: 0) + } + + /// Center the map on a specific location. + /// + /// - Parameters: + /// - 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). + /// - Returns: The constructed MapViewCamera. + public static func center(_ coordinate: CLLocationCoordinate2D, + zoom: Double, + pitch: Double = 90.0, + course: Double = 0) -> MapViewCamera { + + return MapViewCamera(state: .centered, + coordinate: coordinate, + zoom: zoom, + pitch: pitch, + course: course) + } + + public static func userLocation(_ location: CLLocation, + zoom: Double, + pitch: Double = 90.0) -> MapViewCamera { + + return MapViewCamera(state: .userLocation, + coordinate: location.coordinate, + zoom: zoom, + pitch: pitch, + course: location.course) + } + + // 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 + } +} diff --git a/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift b/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift new file mode 100644 index 0000000..fb770e3 --- /dev/null +++ b/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift @@ -0,0 +1,13 @@ +// +// File.swift +// +// +// Created by Jacob Fielding on 12/16/23. +// + +import Foundation + +// TODO: Support MLNStyle as well; having a DSL for that would be nice +public enum MapStyleSource { + case url(URL) +} diff --git a/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift b/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift new file mode 100644 index 0000000..23a788d --- /dev/null +++ b/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift @@ -0,0 +1,22 @@ +// +// File.swift +// +// +// Created by Jacob Fielding on 12/16/23. +// + +import SwiftUI +import MapLibreSwiftDSL + +extension MapView { + + public func mapContent(@MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition]) -> MapView { + switch self.styleSource { + + case .url(let styleUrl): + return MapView(styleURL: styleUrl, + camera: self.camera, + makeMapContent) + } + } +} From 57504cc1508c8c72b52288db8c32f47f56f283d5 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Wed, 20 Dec 2023 20:43:06 -0800 Subject: [PATCH 2/4] Added functionality to MapViewContentBuilder to handle more layer logic --- .../MapViewContentBuilder.swift | 34 +++++++++++++++++++ .../Style Layers/StyleLayerCollection.swift | 8 +++++ Sources/MapLibreSwiftUI/MapView.swift | 9 ++--- .../Models/MapCamera/CameraState.swift | 7 ---- .../{MapStyleSource => }/MapStyleSource.swift | 7 ---- .../MapViewContentViewModifier.swift | 22 ------------ 6 files changed, 44 insertions(+), 43 deletions(-) create mode 100644 Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift create mode 100644 Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift rename Sources/MapLibreSwiftUI/Models/{MapStyleSource => }/MapStyleSource.swift (65%) delete mode 100644 Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift diff --git a/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift b/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift new file mode 100644 index 0000000..7062896 --- /dev/null +++ b/Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift @@ -0,0 +1,34 @@ +import Foundation + +@resultBuilder +public enum MapViewContentBuilder { + + public static func buildBlock(_ layers: [StyleLayerDefinition]...) -> [StyleLayerDefinition] { + return layers.flatMap { $0 } + } + + public static func buildOptional(_ layers: [StyleLayerDefinition]?) -> [StyleLayerDefinition] { + return layers ?? [] + } + + public static func buildExpression(_ layer: StyleLayerDefinition) -> [StyleLayerDefinition] { + return [layer] + } + + public static func buildExpression(_ expression: Void) -> [StyleLayerDefinition] { + return [] + } + + public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] { + return styleCollection.layers + } + + + public static func buildEither(first layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] { + return layer + } + + public static func buildEither(second layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] { + return layer + } +} diff --git a/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift b/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift new file mode 100644 index 0000000..6211396 --- /dev/null +++ b/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift @@ -0,0 +1,8 @@ +import Foundation + +public protocol StyleLayerCollection { + +// associatedtype Layer : StyleLayerDefinition + + @MapViewContentBuilder var layers: [StyleLayerDefinition] { get } +} diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index c90806f..683a785 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -9,7 +9,7 @@ public struct MapView: UIViewRepresentable { public private(set) var camera: Binding? public let styleSource: MapStyleSource - let userLayers: [StyleLayerDefinition] + public let userLayers: [StyleLayerDefinition] public init( styleURL: URL, @@ -203,12 +203,7 @@ public struct MapView: UIViewRepresentable { } } -@resultBuilder -public enum MapViewContentBuilder { - public static func buildBlock(_ layers: StyleLayerDefinition...) -> [StyleLayerDefinition] { - return layers - } -} + struct MapView_Previews: PreviewProvider { static var previews: some View { diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift index f3bebac..0aadb64 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by Jacob Fielding on 12/16/23. -// - import Foundation import MapLibre diff --git a/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift b/Sources/MapLibreSwiftUI/Models/MapStyleSource.swift similarity index 65% rename from Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift rename to Sources/MapLibreSwiftUI/Models/MapStyleSource.swift index fb770e3..8bcd8d3 100644 --- a/Sources/MapLibreSwiftUI/Models/MapStyleSource/MapStyleSource.swift +++ b/Sources/MapLibreSwiftUI/Models/MapStyleSource.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by Jacob Fielding on 12/16/23. -// - import Foundation // TODO: Support MLNStyle as well; having a DSL for that would be nice diff --git a/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift b/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift deleted file mode 100644 index 23a788d..0000000 --- a/Sources/MapLibreSwiftUI/ViewModifiers/MapViewContentViewModifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// -// -// Created by Jacob Fielding on 12/16/23. -// - -import SwiftUI -import MapLibreSwiftDSL - -extension MapView { - - public func mapContent(@MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition]) -> MapView { - switch self.styleSource { - - case .url(let styleUrl): - return MapView(styleURL: styleUrl, - camera: self.camera, - makeMapContent) - } - } -} From 10b26f3b0d524c9a3ac297cd7dd98b3bdc0e6d02 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Wed, 20 Dec 2023 20:45:25 -0800 Subject: [PATCH 3/4] Added functionality to MapViewContentBuilder to handle more layer logic --- .../MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift | 2 -- Sources/MapLibreSwiftUI/MapView.swift | 2 -- 2 files changed, 4 deletions(-) diff --git a/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift b/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift index 6211396..468c967 100644 --- a/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift +++ b/Sources/MapLibreSwiftDSL/Style Layers/StyleLayerCollection.swift @@ -1,8 +1,6 @@ import Foundation public protocol StyleLayerCollection { - -// associatedtype Layer : StyleLayerDefinition @MapViewContentBuilder var layers: [StyleLayerDefinition] { get } } diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 683a785..b376a1f 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -203,8 +203,6 @@ public struct MapView: UIViewRepresentable { } } - - struct MapView_Previews: PreviewProvider { static var previews: some View { let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")! From 8db4709c39ccf9875d08ef02bfb3cc9d7339be10 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Thu, 28 Dec 2023 11:10:27 -0800 Subject: [PATCH 4/4] Revisions based on PR feedback for MapView --- Sources/MapLibreSwiftUI/Examples/Camera.swift | 3 +- Sources/MapLibreSwiftUI/MapView.swift | 56 ++++++++++--------- .../Models/MapCamera/CameraState.swift | 4 +- .../Models/MapCamera/MapViewCamera.swift | 12 ++-- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Sources/MapLibreSwiftUI/Examples/Camera.swift b/Sources/MapLibreSwiftUI/Examples/Camera.swift index b16540f..b628747 100644 --- a/Sources/MapLibreSwiftUI/Examples/Camera.swift +++ b/Sources/MapLibreSwiftUI/Examples/Camera.swift @@ -11,8 +11,9 @@ 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 ?? 0)") + Text("\(camera.coordinate.latitude), \(camera.coordinate.longitude) z \(camera.zoom)") .padding() + .foregroundColor(.white) .background( Rectangle() .foregroundColor(.black) diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index b376a1f..3611320 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -2,18 +2,17 @@ import SwiftUI import InternalUtils import MapLibre import MapLibreSwiftDSL -import MapLibreSwiftUI public struct MapView: UIViewRepresentable { - public private(set) var camera: Binding? + public private(set) var camera: Binding - public let styleSource: MapStyleSource - public let userLayers: [StyleLayerDefinition] + let styleSource: MapStyleSource + let userLayers: [StyleLayerDefinition] public init( styleURL: URL, - camera: Binding? = nil, + camera: Binding = .constant(.default()), @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] } ) { self.styleSource = .url(styleURL) @@ -36,7 +35,7 @@ public struct MapView: UIViewRepresentable { // Storage of variables as they were previously; these are snapshot // every update cycle so we can avoid unnecessary updates private var snapshotUserLayers: [StyleLayerDefinition] = [] - var snapshotCamera: MapViewCamera? + private var snapshotCamera: MapViewCamera? init(parent: MapView) { self.parent = parent @@ -59,13 +58,27 @@ public struct MapView: UIViewRepresentable { public func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) { DispatchQueue.main.async { - self.parent.camera?.wrappedValue = .center(mapView.centerCoordinate, - zoom: mapView.zoomLevel) + self.parent.camera.wrappedValue = .center(mapView.centerCoordinate, + zoom: mapView.zoomLevel) } } // MARK: - Coordinator API + func updateCamera(mapView: MLNMapView, camera: MapViewCamera, animated: Bool) { + guard camera != snapshotCamera else { + // No action - camera has not changed. + return + } + + mapView.setCenter(camera.coordinate, + zoomLevel: camera.zoom, + direction: camera.course, + animated: animated) + + snapshotCamera = camera + } + func updateLayers(mapView: MLNMapView) { // TODO: Figure out how to selectively update layers when only specific props changed. New function in addition to makeMLNStyleLayer? @@ -163,8 +176,10 @@ public struct MapView: UIViewRepresentable { mapView.styleURL = styleURL } - updateMapCamera(mapView, context: context, animated: false) - + context.coordinator.updateCamera(mapView: mapView, + camera: camera.wrappedValue, + animated: false) + // TODO: Make this settable via a modifier mapView.logoView.isHidden = true @@ -180,26 +195,13 @@ public struct MapView: UIViewRepresentable { // FIXME: This should be a more selective update context.coordinator.updateStyleSource(styleSource, mapView: mapView) context.coordinator.updateLayers(mapView: mapView) - + // FIXME: This isn't exactly telling us if the *map* is loaded, and the docs for setCenter say it needs t obe. let isStyleLoaded = mapView.style != nil - updateMapCamera(mapView, context: context, animated: isStyleLoaded) - } - - private func updateMapCamera(_ mapView: MLNMapView, context: Context, animated: Bool) { - guard let newCamera = self.camera?.wrappedValue, - context.coordinator.snapshotCamera != newCamera else { - // Exit early - the camera has not changed. - return - } - - mapView.setCenter(newCamera.coordinate, - zoomLevel: newCamera.zoom, - direction: newCamera.course, - animated: animated) - - context.coordinator.snapshotCamera = newCamera + context.coordinator.updateCamera(mapView: mapView, + camera: camera.wrappedValue, + animated: isStyleLoaded) } } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift index 0aadb64..3860508 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift @@ -8,7 +8,7 @@ public enum CameraState { case centered /// The camera is currently following a location provider. - case userLocation + case trackingUserLocation /// Centered on a bounding box/rectangle. case rect @@ -24,7 +24,7 @@ extension CameraState: Equatable { case (.centered, .centered): return true - case (.userLocation, .userLocation): + case (.trackingUserLocation, .trackingUserLocation): return true case (.rect, .rect): return true diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift index 8e977da..0787544 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -9,12 +9,12 @@ public struct MapViewCamera { public var pitch: Double public var course: CLLocationDirection - /// A backup camera centered at 0.0, 0.0. This is typically used as a backup, + /// A camera centered at 0.0, 0.0. This is typically used as a backup, /// pre-load for an expected camera update (e.g. before a location provider produces /// it's first location). /// /// - Returns: The constructed MapViewCamera. - public static func backup() -> MapViewCamera { + public static func `default`() -> MapViewCamera { return MapViewCamera(state: .centered, coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0), zoom: 10, @@ -42,11 +42,11 @@ public struct MapViewCamera { course: course) } - public static func userLocation(_ location: CLLocation, - zoom: Double, - pitch: Double = 90.0) -> MapViewCamera { + public static func trackUserLocation(_ location: CLLocation, + zoom: Double, + pitch: Double = 90.0) -> MapViewCamera { - return MapViewCamera(state: .userLocation, + return MapViewCamera(state: .trackingUserLocation, coordinate: location.coordinate, zoom: zoom, pitch: pitch,