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

Support Navigation #39

Merged
merged 26 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
113f368
Pitch improvements (#1)
hactar May 15, 2024
347c224
Squashed commit of the following:
hactar May 15, 2024
f91d4b2
add NavigationMapView
Patrick-Kladek May 24, 2024
13323b2
Merge branch 'main' of https://github.com/HudHud-Maps/maplibre-swiftu…
Patrick-Kladek May 24, 2024
be6fa07
add missing modifiers
Patrick-Kladek May 24, 2024
788303e
Add Navigation Target
Patrick-Kladek May 27, 2024
f1afff5
correctly handle callbacks when ending route
Patrick-Kladek May 29, 2024
32a03f8
fix pr comments
Patrick-Kladek May 31, 2024
c1276ab
use Style instead of MapStyleSource
Patrick-Kladek Jun 3, 2024
8793414
rename state to navigationState
Patrick-Kladek Jun 5, 2024
8b9390b
wrap MapView in UIViewController
Patrick-Kladek Jun 5, 2024
00ac500
add generics to MapView
Patrick-Kladek Jun 5, 2024
b6d2b9f
remove maplibre-navigation dependency
Patrick-Kladek Jun 7, 2024
c1ae13b
format code
Patrick-Kladek Jun 7, 2024
0346e36
fix tests
Patrick-Kladek Jun 10, 2024
13a541a
allow injecting custom initialiser
Patrick-Kladek Jun 11, 2024
013ef56
run linter
Patrick-Kladek Jun 11, 2024
40eac45
Merge branch 'stadiamaps:main' into main
Patrick-Kladek Jun 13, 2024
61c26af
Merge branch 'main' of https://github.com/HudHud-Maps/maplibre-swiftu…
Patrick-Kladek Jun 13, 2024
565da23
generic init implementation to keep cleaner callsite
hactar Jul 8, 2024
5ed30bd
linting
hactar Jul 8, 2024
5ab5390
code review
Patrick-Kladek Jul 15, 2024
ebea668
add maplibre-navigation instructs to readme
Patrick-Kladek Jul 15, 2024
aa3f51a
swiftformat
Patrick-Kladek Jul 15, 2024
18e34ac
rename to MLNMapViewController
Patrick-Kladek Jul 15, 2024
aece53f
remove unused modifier
Patrick-Kladek Jul 15, 2024
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1540"
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 = "1510"
LastUpgradeVersion = "1540"
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 = "1510"
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ can move fast without breaking anything important.
* Overlays
* Dynamic styling
* Camera control / animation??
* Navigation
2. Prevent most common classes of mistakes that users make with the lower level APIs (ex: adding the same source twice)
3. Deeper SwiftUI integration (ex: SwiftUI callout views)

Expand All @@ -46,6 +47,45 @@ Then, for each target add either the DSL (for just the DSL) or both (for the Swi
Check out the (super basic) [previews at the bottom of MapView.swift](Sources/MapLibreSwiftUI/MapView.swift)
or more detailed [Examples](Sources/MapLibreSwiftUI/Examples) to see how it works in practice.

## Navigation

If you need to support navigation add https://github.com/HudHud-Maps/maplibre-navigation-ios.git to your Package.swift and add this code:

```swift
import MapboxCoreNavigation
import MapboxNavigation

extension NavigationViewController: MapViewHostViewController {
public typealias MapType = NavigationMapView
}


@State var route: Route?
@State var navigationInProgress: Bool = false

@ViewBuilder
var mapView: some View {
MapView<NavigationViewController>(makeViewController: NavigationViewController(dayStyleURL: self.styleURL), styleURL: self.styleURL, camera: self.$mapStore.camera) {

}
.unsafeMapViewControllerModifier { navigationViewController in
navigationViewController.delegate = self.mapStore
if let route = self.route, self.navigationInProgress == false {
let locationManager = SimulatedLocationManager(route: route)
navigationViewController.startNavigation(with: route, locationManager: locationManager)
self.navigationInProgress = true
} else if self.route == nil, self.navigationInProgress == true {
navigationViewController.endNavigation()
self.navigationInProgress = false
}

navigationViewController.mapView.showsUserLocation = self.showUserLocation && self.mapStore.streetView == .disabled
}
.cameraModifierDisabled(self.route != nil)
}
```
We choose this approach so MapLibreSwiftUI is not depdending on maplibre-navigation as most users don't need it.

## Developer Quick Start

This project uses [`swiftformat`](https://github.com/nicklockwood/SwiftFormat) to automatically handle basic swift formatting
Expand Down
4 changes: 2 additions & 2 deletions Sources/MapLibreSwiftUI/Examples/Gestures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ import SwiftUI
.iconColor(.white)
}
.onTapMapGesture(on: [tappableID], onTapChanged: { _, features in
print("Tapped on \(features.first)")
print("Tapped on \(features.first?.description ?? "<nil>")")
})
.ignoresSafeArea(.all)
}

#Preview("Tappable Countries") {
MapView(styleURL: demoTilesURL)
.onTapMapGesture(on: ["countries-fill"], onTapChanged: { _, features in
print("Tapped on \(features.first)")
print("Tapped on \(features.first?.description ?? "<nil>")")
})
.ignoresSafeArea(.all)
}
2 changes: 1 addition & 1 deletion Sources/MapLibreSwiftUI/Examples/Layers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ let clustered = ShapeSource(identifier: "points", options: [.clustered: true, .c
.predicate(NSPredicate(format: "cluster != YES"))
}
.onTapMapGesture(on: ["simple-circles-non-clusters"], onTapChanged: { _, features in
print("Tapped on \(features.first)")
print("Tapped on \(features.first?.debugDescription ?? "<nil>")")
})
.expandClustersOnTapping(clusteredLayers: [ClusterLayer(
layerIdentifier: "simple-circles-clusters",
Expand Down
6 changes: 3 additions & 3 deletions Sources/MapLibreSwiftUI/Examples/Other.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import SwiftUI
SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(UIImage(systemName: "mappin")!)
}
.unsafeMapViewModifier { mapView in
.unsafeMapViewControllerModifier { viewController in
// Not all properties have modifiers yet. Until they do, you can use this 'escape hatch' to the underlying
// MLNMapView. Be careful: if you modify properties that the DSL controls already, they may be overridden. This
// modifier is a "hack", not a final function.
mapView.logoView.isHidden = false
mapView.compassViewPosition = .topLeft
viewController.mapView.logoView.isHidden = false
viewController.mapView.compassViewPosition = .topLeft
}
}
3 changes: 1 addition & 2 deletions Sources/MapLibreSwiftUI/Examples/Preview Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
import CoreLocation

let switzerland = CLLocationCoordinate2D(latitude: 47.03041, longitude: 8.29470)
let demoTilesURL =
URL(string: "https://demotiles.maplibre.org/style.json")!
public let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Mockable

// NOTE: We should eventually mark the entire protocol @MainActor, but Mockable generates some unsafe code at the moment
@Mockable
protocol MLNMapViewCameraUpdating: AnyObject {
public protocol MLNMapViewCameraUpdating: AnyObject {
@MainActor var userTrackingMode: MLNUserTrackingMode { get set }
@MainActor var minimumPitch: CGFloat { get set }
@MainActor var maximumPitch: CGFloat { get set }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Mockable
import UIKit

@Mockable
protocol UIGestureRecognizing: AnyObject {
public protocol UIGestureRecognizing: AnyObject {
@MainActor var state: UIGestureRecognizer.State { get }
@MainActor func location(in view: UIView?) -> CGPoint
@MainActor func location(ofTouch touchIndex: Int, in view: UIView?) -> CGPoint
Expand Down
88 changes: 58 additions & 30 deletions Sources/MapLibreSwiftUI/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import MapLibre
import MapLibreSwiftDSL
import SwiftUI

public struct MapView: UIViewRepresentable {
public struct MapView<T: MapViewHostViewController>: UIViewControllerRepresentable {
public typealias UIViewControllerType = T
var cameraDisabled: Bool = true

@Binding var camera: MapViewCamera

let makeViewController: () -> T
let styleSource: MapStyleSource
let userLayers: [StyleLayerDefinition]

Expand All @@ -18,7 +22,9 @@ public struct MapView: UIViewRepresentable {

/// 'Escape hatch' to MLNMapView until we have more modifiers.
/// See ``unsafeMapViewModifier(_:)``
var unsafeMapViewModifier: ((MLNMapView) -> Void)?
var unsafeMapViewModifier: ((T.MapType) -> Void)?
Patrick-Kladek marked this conversation as resolved.
Show resolved Hide resolved

var unsafeMapViewControllerModifier: ((T) -> Void)?

var controls: [MapControl] = [
CompassView(),
Expand All @@ -31,96 +37,118 @@ public struct MapView: UIViewRepresentable {
var clusteredLayers: [ClusterLayer]?

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

public func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(
public func makeCoordinator() -> MapViewCoordinator<T> {
MapViewCoordinator<T>(
parent: self,
onGesture: { processGesture($0, $1) },
onViewPortChanged: { onViewPortChanged?($0) }
)
}

public func makeUIView(context: Context) -> MLNMapView {
public func makeUIViewController(context: Context) -> T {
// Create the map view
let mapView = MLNMapView(frame: .zero)
mapView.delegate = context.coordinator
context.coordinator.mapView = mapView
let controller = makeViewController()
controller.mapView.delegate = context.coordinator
context.coordinator.mapView = controller.mapView

// Apply modifiers, suppressing camera update propagation (this messes with setting our initial camera as
// content insets can trigger a change)
context.coordinator.suppressCameraUpdatePropagation = true
applyModifiers(mapView, runUnsafe: false)
applyModifiers(controller, runUnsafe: false)
context.coordinator.suppressCameraUpdatePropagation = false

mapView.locationManager = locationManager
controller.mapView.locationManager = locationManager

switch styleSource {
case let .url(styleURL):
mapView.styleURL = styleURL
controller.mapView.styleURL = styleURL
}

context.coordinator.updateCamera(mapView: mapView,
context.coordinator.updateCamera(mapView: controller.mapView,
camera: $camera.wrappedValue,
animated: false)
mapView.locationManager = mapView.locationManager
controller.mapView.locationManager = controller.mapView.locationManager

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

// Add all gesture recognizers
for gesture in gestures {
registerGesture(mapView, context, gesture: gesture)
registerGesture(controller.mapView, context, gesture: gesture)
}

return mapView
return controller
}

public func updateUIView(_ mapView: MLNMapView, context: Context) {
public func updateUIViewController(_ uiViewController: T, context: Context) {
context.coordinator.parent = self

applyModifiers(mapView, runUnsafe: true)
applyModifiers(uiViewController, runUnsafe: true)

// FIXME: This should be a more selective update
context.coordinator.updateStyleSource(styleSource, mapView: mapView)
context.coordinator.updateLayers(mapView: mapView)
context.coordinator.updateStyleSource(styleSource, mapView: uiViewController.mapView)
context.coordinator.updateLayers(mapView: uiViewController.mapView)

// FIXME: This isn't exactly telling us if the *map* is loaded, and the docs for setCenter say it needs to be.
let isStyleLoaded = mapView.style != nil
let isStyleLoaded = uiViewController.mapView.style != nil

context.coordinator.updateCamera(mapView: mapView,
camera: $camera.wrappedValue,
animated: isStyleLoaded)
if cameraDisabled == false {
Patrick-Kladek marked this conversation as resolved.
Show resolved Hide resolved
context.coordinator.updateCamera(mapView: uiViewController.mapView,
camera: $camera.wrappedValue,
animated: isStyleLoaded)
}
}

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

// Assume all controls are hidden by default (so that an empty list returns a map with no controls)
mapView.logoView.isHidden = true
mapView.compassView.isHidden = true
mapView.attributionButton.isHidden = true
mapViewController.mapView.logoView.isHidden = true
mapViewController.mapView.compassView.isHidden = true
mapViewController.mapView.attributionButton.isHidden = true

// Apply each control configuration
for control in controls {
control.configureMapView(mapView)
control.configureMapView(mapViewController.mapView)
}

if runUnsafe {
unsafeMapViewModifier?(mapView)
unsafeMapViewControllerModifier?(mapViewController)
}
}
}

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

#Preview {
MapView(styleURL: demoTilesURL)
.ignoresSafeArea(.all)
Expand Down
17 changes: 17 additions & 0 deletions Sources/MapLibreSwiftUI/MapViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import MapLibre
import UIKit

public protocol MapViewHostViewController: UIViewController {
associatedtype MapType: MLNMapView
var mapView: MapType { get }
}

public final class MapViewController: UIViewController, MapViewHostViewController {
public var mapView: MLNMapView {

Check warning on line 10 in Sources/MapLibreSwiftUI/MapViewController.swift

View workflow job for this annotation

GitHub Actions / test (MapLibreSwiftUI-Package, platform=iOS Simulator,name=iPhone 15,OS=17.2)

main actor-isolated property 'mapView' cannot be used to satisfy nonisolated protocol requirement

Check warning on line 10 in Sources/MapLibreSwiftUI/MapViewController.swift

View workflow job for this annotation

GitHub Actions / test (MapLibreSwiftUI-Package, platform=iOS Simulator,name=iPhone 15,OS=17.2)

main actor-isolated property 'mapView' cannot be used to satisfy nonisolated protocol requirement
view as! MLNMapView
}

override public func loadView() {
view = MLNMapView(frame: .zero)
}
}
10 changes: 4 additions & 6 deletions Sources/MapLibreSwiftUI/MapViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import MapLibre
import MapLibreSwiftDSL

public class MapViewCoordinator: NSObject {
public class MapViewCoordinator<T: MapViewHostViewController>: NSObject, MLNMapViewDelegate {
// This must be weak, the UIViewRepresentable owns the MLNMapView.
weak var mapView: MLNMapView?
var parent: MapView
var parent: MapView<T>

// Storage of variables as they were previously; these are snapshot
// every update cycle so we can avoid unnecessary updates
Expand All @@ -22,7 +22,7 @@
var onGesture: (MLNMapView, UIGestureRecognizer) -> Void
var onViewPortChanged: (MapViewPort) -> Void

init(parent: MapView,
init(parent: MapView<T>,
onGesture: @escaping (MLNMapView, UIGestureRecognizer) -> Void,
onViewPortChanged: @escaping (MapViewPort) -> Void)
{
Expand Down Expand Up @@ -296,11 +296,9 @@
}
}
}
}

// MARK: - MLNMapViewDelegate
// MARK: - MLNMapViewDelegate

extension MapViewCoordinator: MLNMapViewDelegate {
public func mapView(_: MLNMapView, didFinishLoading mglStyle: MLNStyle) {
addLayers(to: mglStyle)
onStyleLoaded?(mglStyle)
Expand Down Expand Up @@ -353,7 +351,7 @@
// FIXME: CI complains about MainActor.assumeIsolated being unavailable before iOS 17, despite building on iOS 17.2... This is an epic hack to fix it for now. I can only assume this is an issue with Xcode pre-15.3
// TODO: We could put this in regionIsChangingWith if we calculate significant change/debounce.
Task { @MainActor in
updateViewPort(mapView: mapView, reason: reason)

Check warning on line 354 in Sources/MapLibreSwiftUI/MapViewCoordinator.swift

View workflow job for this annotation

GitHub Actions / test (MapLibreSwiftUI-Package, platform=iOS Simulator,name=iPhone 15,OS=17.2)

capture of 'self' with non-sendable type 'MapViewCoordinator<T>' in a `@Sendable` closure
}

guard !suppressCameraUpdatePropagation else {
Expand All @@ -362,7 +360,7 @@

// FIXME: CI complains about MainActor.assumeIsolated being unavailable before iOS 17, despite building on iOS 17.2... This is an epic hack to fix it for now. I can only assume this is an issue with Xcode pre-15.3
Task { @MainActor in
updateParentCamera(mapView: mapView, reason: reason)

Check warning on line 363 in Sources/MapLibreSwiftUI/MapViewCoordinator.swift

View workflow job for this annotation

GitHub Actions / test (MapLibreSwiftUI-Package, platform=iOS Simulator,name=iPhone 15,OS=17.2)

capture of 'self' with non-sendable type 'MapViewCoordinator<T>' in a `@Sendable` closure
}
}

Expand Down
Loading
Loading