Skip to content

Commit

Permalink
Merge pull request #12 from Rallista/feat/map-camera-and-layer-mods
Browse files Browse the repository at this point in the history
Feat/map camera and layer mods
  • Loading branch information
ianthetechie authored Jan 2, 2024
2 parents 6199f3f + 8db4709 commit 14da8c2
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ let package = Package(
),
.binaryTarget(
name: "MapLibre",
url: "https://github.com/maplibre/maplibre-native/releases/download/ios-v6.0.0-preda45706601c7ccc6d922a8fcddfc62ff7c8f480d/MapLibre.dynamic.xcframework.zip",
checksum: "37e621c0c7c1f589f0a125816155ba443000d78b80649d85a9b8b3d19144836c"
url: "https://github.com/maplibre/maplibre-native/releases/download/ios-v6.0.0-pre9fbcb031f019048f21fdfcb57b80f4451cdecfd9/MapLibre.dynamic.xcframework.zip",
checksum: "929bbc3f24740df360bf447acf08bd102cf4baf12a6bc099f42e5fb8b5130cf4"
),
.target(
name: "MapLibreSwiftUI",
Expand Down
34 changes: 34 additions & 0 deletions Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public protocol StyleLayerCollection {

@MapViewContentBuilder var layers: [StyleLayerDefinition] { get }
}
22 changes: 12 additions & 10 deletions Sources/MapLibreSwiftUI/Examples/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@ import SwiftUI
private let switzerland = CLLocationCoordinate2D(latitude: 46.801111, longitude: 8.226667)

struct CameraDirectManipulationPreview: View {
@State private var camera = MapView.Camera.centerAndZoom(switzerland, 4)
@State private var camera = MapViewCamera.center(switzerland, zoom: 4)

let styleURL: URL

var body: some View {
MapView(styleURL: styleURL, camera: $camera)
.overlay(alignment: .bottom, content: {
switch camera {
case .centerAndZoom(let coord, let zoom):
Text("\(coord.latitude), \(coord.longitude) z \(zoom ?? 0)")
.padding()
.background(in: RoundedRectangle(cornerRadius: 8),
fillStyle: .init())
.padding(.bottom, 42)
}
Text("\(camera.coordinate.latitude), \(camera.coordinate.longitude) z \(camera.zoom)")
.padding()
.foregroundColor(.white)
.background(
Rectangle()
.foregroundColor(.black)
.cornerRadius(8)
)

.padding(.bottom, 42)
})
.task {
try! await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)

camera = MapView.Camera.centerAndZoom(switzerland, 6)
camera = MapViewCamera.center(switzerland, zoom: 6)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapLibreSwiftUI/Examples/Polyline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ struct PolylinePreview: View {
let styleURL: URL

var body: some View {
MapView(styleURL: styleURL, initialCamera: MapView.Camera.centerAndZoom(samplePedestrianWaypoints.first!, 14)) {
MapView(styleURL: styleURL, initialCamera: MapViewCamera.center(samplePedestrianWaypoints.first!, zoom: 14)) {
// Note: This line does not add the source to the style as if it
// were a statement in an imperative programming language.
// The source is added automatically if a layer references it.
Expand Down
78 changes: 38 additions & 40 deletions Sources/MapLibreSwiftUI/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ import InternalUtils
import MapLibre
import MapLibreSwiftDSL


public struct MapView: UIViewRepresentable {
// TODO: Support MLNStyle as well; having a DSL for that would be nice
enum MapStyleSource {
case url(URL)
}

public enum Camera {
case centerAndZoom(CLLocationCoordinate2D, Double?)
}

var camera: Binding<Camera>?

public private(set) var camera: Binding<MapViewCamera>

let styleSource: MapStyleSource
let userLayers: [StyleLayerDefinition]

public init(styleURL: URL, camera: Binding<Camera>? = nil, @MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }) {
public init(
styleURL: URL,
camera: Binding<MapViewCamera> = .constant(.default()),
@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)
}

Expand All @@ -36,7 +35,8 @@ 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] = []

private var snapshotCamera: MapViewCamera?

init(parent: MapView) {
self.parent = parent
}
Expand All @@ -58,12 +58,27 @@ 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)
}
}

// 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?

Expand Down Expand Up @@ -161,8 +176,10 @@ public struct MapView: UIViewRepresentable {
mapView.styleURL = styleURL
}

updateMapCamera(mapView, animated: false)

context.coordinator.updateCamera(mapView: mapView,
camera: camera.wrappedValue,
animated: false)

// TODO: Make this settable via a modifier
mapView.logoView.isHidden = true

Expand All @@ -178,32 +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, animated: isStyleLoaded)
}

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)
}
}
}
}
}

@resultBuilder
public enum MapViewContentBuilder {
public static func buildBlock(_ layers: StyleLayerDefinition...) -> [StyleLayerDefinition] {
return layers
context.coordinator.updateCamera(mapView: mapView,
camera: camera.wrappedValue,
animated: isStyleLoaded)
}
}

Expand Down
37 changes: 37 additions & 0 deletions Sources/MapLibreSwiftUI/Models/MapCamera/CameraState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 trackingUserLocation

/// 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 (.trackingUserLocation, .trackingUserLocation):
return true
case (.rect, .rect):
return true
case (.showcase, .showcase):
return true
default:
return false
}
}
}
69 changes: 69 additions & 0 deletions Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift
Original file line number Diff line number Diff line change
@@ -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 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 `default`() -> 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 trackUserLocation(_ location: CLLocation,
zoom: Double,
pitch: Double = 90.0) -> MapViewCamera {

return MapViewCamera(state: .trackingUserLocation,
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
}
}
6 changes: 6 additions & 0 deletions Sources/MapLibreSwiftUI/Models/MapStyleSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

// TODO: Support MLNStyle as well; having a DSL for that would be nice
public enum MapStyleSource {
case url(URL)
}

0 comments on commit 14da8c2

Please sign in to comment.