Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content insets and more modifiers #28

Merged
merged 16 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions Sources/MapLibreSwiftUI/Examples/Camera.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import CoreLocation
import SwiftUI

private let switzerland = CLLocationCoordinate2D(latitude: 46.801111, longitude: 8.226667)

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

let styleURL: URL
var onStyleLoaded: (() -> Void)? = nil
var targetCameraAfterDelay: MapViewCamera? = nil

var body: some View {
MapView(styleURL: styleURL, camera: $camera)
.onStyleLoaded { _ in
print("Style is loaded")
onStyleLoaded?()
}
.overlay(alignment: .bottom, content: {
Text("\(String(describing: camera.state)) z \(camera.zoom)")
Text("\(String(describing: camera.state))")
.padding()
.foregroundColor(.white)
.background(
Expand All @@ -27,16 +25,19 @@ struct CameraDirectManipulationPreview: View {
.padding(.bottom, 42)
})
.task {
try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
if let targetCameraAfterDelay {
try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)

camera = MapViewCamera.center(switzerland, zoom: 6)
camera = targetCameraAfterDelay
}
}
}
}

#Preview("Camera Preview") {
#Preview("Camera Zoom after delay") {
CameraDirectManipulationPreview(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!
styleURL: demoTilesURL,
targetCameraAfterDelay: .center(switzerland, zoom: 6)
)
.ignoresSafeArea(.all)
}
2 changes: 0 additions & 2 deletions Sources/MapLibreSwiftUI/Examples/Layers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import MapLibre
import MapLibreSwiftDSL
import SwiftUI

let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!

// A collection of points with various
// attributes
let pointSource = ShapeSource(identifier: "points") {
Expand Down
4 changes: 1 addition & 3 deletions Sources/MapLibreSwiftUI/Examples/Polyline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct PolylinePreview: View {

var body: some View {
MapView(styleURL: styleURL,
constantCamera: .center(samplePedestrianWaypoints.first!, zoom: 14))
camera: .constant(.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.
Expand Down Expand Up @@ -43,8 +43,6 @@ struct PolylinePreview: View {

struct Polyline_Previews: PreviewProvider {
static var previews: some View {
let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!

PolylinePreview(styleURL: demoTilesURL)
.ignoresSafeArea(.all)
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/MapLibreSwiftUI/Examples/Preview Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This file contains helpers that are used in the SwiftUI preview examples
import CoreLocation

let switzerland = CLLocationCoordinate2D(latitude: 47.03041, longitude: 8.29470)
let demoTilesURL =
URL(string: "https://demotiles.maplibre.org/style.json")!
33 changes: 33 additions & 0 deletions Sources/MapLibreSwiftUI/Examples/User Location.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import CoreLocation
import SwiftUI

private let locationManager = StaticLocationManager(initialLocation: CLLocation(
coordinate: switzerland,
altitude: 0,
horizontalAccuracy: 1,
verticalAccuracy: 1,
course: 8,
speed: 28,
timestamp: Date()
))

#Preview("Track user location") {
MapView(
styleURL: demoTilesURL,
camera: .constant(.trackUserLocation(zoom: 4, pitch: .fixed(45))),
locationManager: locationManager
)
.mapViewContentInset(.init(top: 450, left: 0, bottom: 0, right: 0))
.ignoresSafeArea(.all)
}

#Preview("Track user location with Course") {
MapView(
styleURL: demoTilesURL,
camera: .constant(.trackUserLocationWithCourse(zoom: 4, pitch: .fixed(45))),
locationManager: locationManager
)
.mapViewContentInset(.init(top: 450, left: 0, bottom: 0, right: 0))
.hideCompassView()
.ignoresSafeArea(.all)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension MapView {
sender: UIGestureRecognizing) -> MapGestureContext
{
// Build the context of the gesture's event.
var point: CGPoint = switch gesture.method {
let point: CGPoint = switch gesture.method {
case let .tap(numberOfTaps: numberOfTaps):
// Calculate the CGPoint of the last gesture tap
sender.location(ofTouch: numberOfTaps - 1, in: mapView)
Expand Down
68 changes: 50 additions & 18 deletions Sources/MapLibreSwiftUI/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,26 @@ public struct MapView: UIViewRepresentable {
var gestures = [MapGesture]()
var onStyleLoaded: ((MLNStyle) -> Void)?

public var mapViewContentInset: UIEdgeInsets = .zero
public var isLogoViewHidden = false
public var isCompassViewHidden = false

/// 'Escape hatch' to MLNMapView until we have more modifiers.
/// See ``unsafeMapViewModifier(_:)``
var unsafeMapViewModifier: ((MLNMapView) -> Void)?

private var locationManager: MLNLocationManager?

public init(
styleURL: URL,
camera: Binding<MapViewCamera> = .constant(.default()),
locationManager: MLNLocationManager? = nil,
@MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
styleSource = .url(styleURL)
_camera = camera
userLayers = makeMapContent()
}

public init(
styleURL: URL,
constantCamera: MapViewCamera,
@MapViewContentBuilder _ makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.init(styleURL: styleURL,
camera: .constant(constantCamera),
makeMapContent)
Comment on lines -29 to -36
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially controversial breaking change: I'm removing this since it's not that hard to do this yourself (just a few more characters). The annoyance of having to keep the convenience initializers in sync with the main one will get annoying over time, so I'm of a mind to remove this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @hactar since I know you're a somewhat active user.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - not using that one - and don't worry about me, I am aware that I am using a library at version 0.0.6, I'm expecting to be hit with a lot of breaking changes 😄

self.locationManager = locationManager
}

public func makeCoordinator() -> MapViewCoordinator {
Expand All @@ -49,6 +47,10 @@ public struct MapView: UIViewRepresentable {
mapView.delegate = context.coordinator
context.coordinator.mapView = mapView

applyModifiers(mapView, runUnsafe: false)

mapView.locationManager = locationManager

switch styleSource {
case let .url(styleURL):
mapView.styleURL = styleURL
Expand All @@ -58,9 +60,6 @@ public struct MapView: UIViewRepresentable {
camera: $camera.wrappedValue,
animated: false)

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

// Link the style loaded to the coordinator that emits the delegate event.
context.coordinator.onStyleLoaded = onStyleLoaded

Expand All @@ -75,11 +74,7 @@ public struct MapView: UIViewRepresentable {
public func updateUIView(_ mapView: MLNMapView, context: Context) {
context.coordinator.parent = self

// MARK: Modifiers

unsafeMapViewModifier?(mapView)

// MARK: End Modifiers
applyModifiers(mapView, runUnsafe: true)

// FIXME: This should be a more selective update
context.coordinator.updateStyleSource(styleSource, mapView: mapView)
Expand All @@ -92,6 +87,43 @@ public struct MapView: UIViewRepresentable {
camera: $camera.wrappedValue,
animated: isStyleLoaded)
}

private func applyModifiers(_ mapView: MLNMapView, runUnsafe: Bool) {
mapView.contentInset = mapViewContentInset

mapView.logoView.isHidden = isLogoViewHidden
mapView.compassView.isHidden = isCompassViewHidden

if runUnsafe {
unsafeMapViewModifier?(mapView)
}
}
}

public extension MapView {
func mapViewContentInset(_ inset: UIEdgeInsets) -> Self {
var result = self

result.mapViewContentInset = inset

return result
}

func hideLogoView() -> Self {
var result = self

result.isLogoViewHidden = true

return result
}

func hideCompassView() -> Self {
var result = self

result.isCompassViewHidden = true

return result
}
Archdoog marked this conversation as resolved.
Show resolved Hide resolved
}

#Preview {
Expand Down
75 changes: 46 additions & 29 deletions Sources/MapLibreSwiftUI/MapViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,32 @@ public class MapViewCoordinator: NSObject {
}

switch camera.state {
case let .centered(onCoordinate: coordinate):
case let .centered(onCoordinate: coordinate, zoom: zoom, pitch: pitch, direction: direction):
mapView.userTrackingMode = .none
mapView.setCenter(coordinate,
zoomLevel: camera.zoom,
direction: camera.direction,
zoomLevel: zoom,
direction: direction,
animated: animated)
case .trackingUserLocation:
mapView.minimumPitch = pitch.rangeValue.lowerBound
mapView.maximumPitch = pitch.rangeValue.upperBound
case let .trackingUserLocation(zoom: zoom, pitch: pitch):
mapView.userTrackingMode = .follow
mapView.setZoomLevel(camera.zoom, animated: false)
case .trackingUserLocationWithHeading:
// Needs to be non-animated or else it messes up following
mapView.setZoomLevel(zoom, animated: false)
mapView.minimumPitch = pitch.rangeValue.lowerBound
mapView.maximumPitch = pitch.rangeValue.upperBound
case let .trackingUserLocationWithHeading(zoom: zoom, pitch: pitch):
mapView.userTrackingMode = .followWithHeading
mapView.setZoomLevel(camera.zoom, animated: false)
case .trackingUserLocationWithCourse:
// Needs to be non-animated or else it messes up following
mapView.setZoomLevel(zoom, animated: false)
mapView.minimumPitch = pitch.rangeValue.lowerBound
mapView.maximumPitch = pitch.rangeValue.upperBound
case let .trackingUserLocationWithCourse(zoom: zoom, pitch: pitch):
mapView.userTrackingMode = .followWithCourse
mapView.setZoomLevel(camera.zoom, animated: false)
// Needs to be non-animated or else it messes up following
mapView.setZoomLevel(zoom, animated: false)
mapView.minimumPitch = pitch.rangeValue.lowerBound
mapView.maximumPitch = pitch.rangeValue.upperBound
case let .rect(boundingBox, padding):
mapView.setVisibleCoordinateBounds(boundingBox,
edgePadding: padding,
Expand All @@ -73,10 +84,6 @@ public class MapViewCoordinator: NSObject {
break
}

// Set the correct pitch range.
mapView.minimumPitch = camera.pitch.rangeValue.lowerBound
mapView.maximumPitch = camera.pitch.rangeValue.upperBound

snapshotCamera = camera
}

Expand Down Expand Up @@ -188,25 +195,35 @@ extension MapViewCoordinator: MLNMapViewDelegate {

/// The MapView's region has changed with a specific reason.
public func mapView(_ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated _: Bool) {
// Validate that the mapView.userTrackingMode still matches our desired camera state for each tracking type.
let isFollowing = parent.camera.state == .trackingUserLocation && mapView.userTrackingMode == .follow
let isFollowingHeading = parent.camera.state == .trackingUserLocationWithHeading && mapView
.userTrackingMode == .followWithHeading
let isFollowingCourse = parent.camera.state == .trackingUserLocationWithCourse && mapView
.userTrackingMode == .followWithCourse

// 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 matches
// the mapView's userTrackingMode
if isFollowing || isFollowingHeading || isFollowingCourse {
// User tracking is still active, we can ignore camera updates until we unset/fail this boolean check
// detach and revert to a .centered camera. If any one of these is true, the desired camera state still
// matches the mapView's userTrackingMode
let isProgrammaticallyTracking: Bool = switch parent.camera.state {
case .centered(onCoordinate: _):
false
case .trackingUserLocation:
mapView.userTrackingMode == .follow
Archdoog marked this conversation as resolved.
Show resolved Hide resolved
case .trackingUserLocationWithHeading:
mapView.userTrackingMode == .followWithHeading
case .trackingUserLocationWithCourse:
mapView.userTrackingMode == .followWithCourse
case .rect(boundingBox: _, edgePadding: _):
false
case .showcase(shapeCollection: _):
false
Archdoog marked this conversation as resolved.
Show resolved Hide resolved
}

if isProgrammaticallyTracking {
// Programmatic tracking is still active, we can ignore camera updates until we unset/fail this boolean
// check
return
}

// The user's desired camera is not a user tracking method, now we need to publish the MLNMapView's camera state
// to the MapView camera binding.
parent.camera = .center(mapView.centerCoordinate,
zoom: mapView.zoomLevel,
reason: CameraChangeReason(reason))
DispatchQueue.main.async {
// Publish the MLNMapView's "raw" camera state to the MapView camera binding.
self.parent.camera = .center(mapView.centerCoordinate,
zoom: mapView.zoomLevel,
reason: CameraChangeReason(reason))
}
}
}
Loading
Loading