mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Open map from timeline (#1199)
* Add navigation to expaneded map * Add MapLibreMapView.Options * Add AppActivityView * Add ShareToMapsAppActivity * Add share sheet presentation * Add localisations * Cleanup * Fix UT build errors * Revert breaking change * Fix UIView setup * Add support for location’s description * Show popover on iPad * Restore assets * More cleanup
This commit is contained in:
@@ -18,83 +18,42 @@ import Foundation
|
||||
import Mapbox
|
||||
import SwiftUI
|
||||
|
||||
/// Base class to handle a map annotation
|
||||
class LocationAnnotation: NSObject, MGLAnnotation {
|
||||
// MARK: - Properties
|
||||
|
||||
// Title property is needed to enable annotation selection and callout view showing
|
||||
var title: String?
|
||||
|
||||
final class LocationAnnotation: NSObject, MGLAnnotation {
|
||||
let coordinate: CLLocationCoordinate2D
|
||||
let anchorPoint: CGPoint
|
||||
let view: AnyView
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(coordinate: CLLocationCoordinate2D) {
|
||||
init(coordinate: CLLocationCoordinate2D,
|
||||
anchorPoint: CGPoint = .init(x: 0.5, y: 0.5),
|
||||
@ViewBuilder label: () -> some View) {
|
||||
self.coordinate = coordinate
|
||||
self.anchorPoint = anchorPoint
|
||||
view = AnyView(label())
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
/// POI map annotation
|
||||
class PinLocationAnnotation: LocationAnnotation { }
|
||||
|
||||
class LocationAnnotationView: MGLUserLocationAnnotationView {
|
||||
private enum Constants {
|
||||
static let defaultFrame = CGRect(x: 0, y: 0, width: 46, height: 46)
|
||||
}
|
||||
|
||||
final class LocationAnnotationView: MGLUserLocationAnnotationView {
|
||||
// MARK: - Setup
|
||||
|
||||
override init(annotation: MGLAnnotation?, reuseIdentifier: String?) {
|
||||
super.init(annotation: annotation, reuseIdentifier:
|
||||
reuseIdentifier)
|
||||
frame = Constants.defaultFrame
|
||||
}
|
||||
|
||||
convenience init(userPinLocationAnnotation: MGLAnnotation) {
|
||||
self.init(annotation: userPinLocationAnnotation, reuseIdentifier: "userPinLocation")
|
||||
|
||||
addUserView()
|
||||
}
|
||||
|
||||
convenience init(pinLocationAnnotation: PinLocationAnnotation) {
|
||||
self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil)
|
||||
|
||||
addPinView()
|
||||
convenience init(annotation: LocationAnnotation) {
|
||||
self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
|
||||
let view: UIView = UIHostingController(rootView: annotation.view).view
|
||||
view.backgroundColor = .clear
|
||||
view.anchorPoint = annotation.anchorPoint
|
||||
addSubview(view)
|
||||
view.bounds.size = view.intrinsicContentSize
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func addUserView() {
|
||||
guard let pinView = UIHostingController(rootView: Image(systemName: "circle.fill")
|
||||
.resizable()
|
||||
.foregroundColor(.compound.iconPrimary)).view else {
|
||||
return
|
||||
}
|
||||
|
||||
addMarkerView(pinView)
|
||||
}
|
||||
|
||||
private func addPinView() {
|
||||
guard let pinView = UIHostingController(rootView: Image(systemName: "mappin")
|
||||
.resizable()
|
||||
.foregroundColor(.compound.iconPrimary)).view else {
|
||||
return
|
||||
}
|
||||
|
||||
addMarkerView(pinView)
|
||||
}
|
||||
|
||||
private func addMarkerView(_ markerView: UIView) {
|
||||
markerView.backgroundColor = .clear
|
||||
|
||||
addSubview(markerView)
|
||||
|
||||
markerView.frame = bounds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,19 @@ import Mapbox
|
||||
import SwiftUI
|
||||
|
||||
struct MapLibreMapView: UIViewRepresentable {
|
||||
// MARK: - Constants
|
||||
|
||||
private enum Constants {
|
||||
static let mapZoomLevel = 15.0
|
||||
static let mapZoomLevelWithoutPermission = 5.0
|
||||
struct Options {
|
||||
/// The initial zoom level
|
||||
let zoomLevel: Double
|
||||
/// The initial map center
|
||||
let mapCenter: CLLocationCoordinate2D?
|
||||
/// Map annotations
|
||||
let annotations: [LocationAnnotation]
|
||||
|
||||
init(zoomLevel: Double, mapCenter: CLLocationCoordinate2D? = nil, annotations: [LocationAnnotation] = []) {
|
||||
self.zoomLevel = zoomLevel
|
||||
self.mapCenter = mapCenter
|
||||
self.annotations = annotations
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -31,6 +39,8 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let builder: MapTilerStyleBuilderProtocol
|
||||
|
||||
let options: Options
|
||||
|
||||
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
var showsUserLocationMode: ShowUserLocationMode = .hide
|
||||
@@ -52,12 +62,13 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan))
|
||||
panGesture.delegate = context.coordinator
|
||||
mapView.addGestureRecognizer(panGesture)
|
||||
mapView.zoomLevel = Constants.mapZoomLevelWithoutPermission
|
||||
setupMap(mapView: mapView, with: options)
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ mapView: MGLMapView, context: Context) {
|
||||
mapView.removeAllAnnotations()
|
||||
mapView.addAnnotations(options.annotations)
|
||||
|
||||
if colorScheme == .dark {
|
||||
mapView.styleURL = builder.dynamicMapURL(for: .dark)
|
||||
@@ -73,6 +84,13 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupMap(mapView: MGLMapView, with options: Options) {
|
||||
mapView.zoomLevel = options.zoomLevel
|
||||
if let mapCenter = options.mapCenter {
|
||||
mapView.centerCoordinate = mapCenter
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMapView() -> MGLMapView {
|
||||
let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light))
|
||||
@@ -85,7 +103,7 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
|
||||
private func showUserLocation(in mapView: MGLMapView) {
|
||||
switch showsUserLocationMode {
|
||||
case .follow:
|
||||
case .showAndFollow:
|
||||
mapView.showsUserLocation = true
|
||||
mapView.userTrackingMode = .follow
|
||||
case .show:
|
||||
@@ -115,12 +133,10 @@ extension MapLibreMapView {
|
||||
// MARK: - MGLMapViewDelegate
|
||||
|
||||
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
|
||||
if let pinLocationAnnotation = annotation as? PinLocationAnnotation {
|
||||
return LocationAnnotationView(pinLocationAnnotation: pinLocationAnnotation)
|
||||
} else if annotation is MGLUserLocation {
|
||||
return LocationAnnotationView(userPinLocationAnnotation: annotation)
|
||||
guard let annotation = annotation as? LocationAnnotation else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return LocationAnnotationView(annotation: annotation)
|
||||
}
|
||||
|
||||
func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) {
|
||||
@@ -143,7 +159,10 @@ extension MapLibreMapView {
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) {
|
||||
mapLibreView.mapCenterCoordinate = mapView.centerCoordinate
|
||||
// Fixes: "Publishing changes from within view updates is not allowed, this will cause undefined behavior."
|
||||
DispatchQueue.main.async { [mapLibreView] in
|
||||
mapLibreView.mapCenterCoordinate = mapView.centerCoordinate
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Callout
|
||||
|
||||
@@ -20,10 +20,10 @@ import Foundation
|
||||
Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
*/
|
||||
enum ShowUserLocationMode {
|
||||
/// this mode will show the user pin in map and track him, panning the map automatically
|
||||
case follow
|
||||
/// this mode will show the user pin in map
|
||||
case show
|
||||
/// this mode will show the user pin in map and track him, panning the map automatically
|
||||
case showAndFollow
|
||||
/// this mode will not show the user pin in map
|
||||
case hide
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user