Swift UI - User Location is not centered on mapkit, resizing Map jumps back to location - iphone

I want to build a map which shows the user location, the zoom scale and can be switched between standard maptype and hybrid maptype.
For testing reasons, I didn't integrate the mapstyle-picker yet. I helped me out by implementing the map.mapType variable, but that doesn't work.
Another problem I have is about the user location: I integrated a LocationManager class which returns the actual position - that works - but if I scroll or zoom on the map, after 5 seconds the screen jumps back to the user location.
I would be glad if you could help me with that. I attach my both files.
Thanks for your help!
UPDATE
So after I searched in stackoverflow I found this thread, SwiftUI mapkit set region to user's current location and implemented these into my code. But now I've the problem, that I didn't see my actual position when the map starts as centered view, I see only the hardcoded one.
MapModel.swift
struct MapModel: UIViewRepresentable {
#Binding var region: MKCoordinateRegion
var mapType : MKMapType
var userTracking: MKUserTrackingMode
var showsUserLocation: Bool
// var annotation: GCModel
init(
region: Binding<MKCoordinateRegion>,
mapType: MKMapType,
userTrackingMode: MKUserTrackingMode,
showsUserLocation: Bool = true
// annotation: GCModel = GCModel(title: "", coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0))
){
self._region = region
self.mapType = mapType
self.userTracking = userTrackingMode
self.showsUserLocation = showsUserLocation
// self.annotation = annotation
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
mapView.showsUserLocation = showsUserLocation
mapView.userTrackingMode = userTracking
mapView.delegate = context.coordinator
// Add annotation to the map
// mapView.addAnnotation(annotation.pointAnnotation)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
// Update your region so that it is now your new region
mapView.setRegion(region, animated: false)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapModel
init(_ parent: MapModel) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// We should handle dequeue of annotation view's properly so we have to write this boiler plate.
// This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
// MKAnnotationView from our annotation.
guard annotation is MKPointAnnotation else { return nil }
let identifier = "Annotation"
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.canShowCallout = true
return annotationView
}
annotationView.annotation = annotation
return annotationView
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// We need to update the region when the user changes it
// otherwise when we zoom the mapview will return to its original region
DispatchQueue.main.async {
self.parent.region = mapView.region
}
}
}
}
MapView.swift
struct MapView: View {
#State var trackingMode: MKUserTrackingMode = .follow
#ObservedObject private var managerDelegate = LocationManager()
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
MapModel(
region: $managerDelegate.region,
mapType: mapType,
userTrackingMode: trackingMode,
showsUserLocation: true
).edgesIgnoringSafeArea([.bottom,.top])
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
.opacity(0.5)
Spacer()
}
}
}

Related

Adding annotations to MapKit in UIRepresentable

I'm working in SwiftUI and using MKMapView to show a map, annotations, and overlays on a view.
I was using the new Map() but the lack of overlay support has pulled me back to UIKit Representable.
When I was using Map it was easy to add annotations, but when using UIKitRepresentable I'm a bit confused on where to put the data, and how to make annotations from an array pulled from a network call.
Everything I've read has been either in Obj-C or adding a single annotation point. I'm trying to add (at present ~800) which is why I wanted to take advantage of the MKMapView in its reusability and clustering.
This is what I have at the moment:
struct UIMapView: UIViewRepresentable {
#EnvironmentObject var dataModel: DataModel
#EnvironmentObject var mapViewModel: MapViewModel
func makeCoordinator() -> Coordinator { Coordinator() }
func makeUIView(context: Context) -> MKMapView {
let view = mapViewModel.mapView
drawOverlayRing(view: view)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {}
class Coordinator: NSObject, MKMapViewDelegate {
func mapView(_ map: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let identifier = "pinAnnotation"
var annotationView = map.dequeueReusableAnnotationView(
withIdentifier: identifier
) as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(
annotation: annotation,
reuseIdentifier: identifier
)
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
return annotationView
}
}
func getAnnotations(view: MKMapView) {
for location in dataModel.locations {
let annotation = MKPointAnnotation()
annotation.title = location.title
annotation.coordinate = CLLocationCoordinate2D(
latitude: location.latitude,
longitude: location.longitude
)
view.addAnnotation(annotation)
}
}
}
I sometimes put my annotations in updateUIView, something like this:
func updateUIView() {
// remove the old ones
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotations(toMapAnnotations(locations: dataModel.locations))
}
func toMapAnnotations(locations: [CLLocationCoordinate2D]) -> [MapAnnotation] {
return locations.map { MapAnnotation(location: $0) }
}
final class MapAnnotation: NSObject, MKAnnotation {...}
The code will be provided later, since updateUIView is called multiple times.
I think it is best to avoid calling addAnnotations.

How to add polylines and annotations from user taps in SwiftUI

I am trying to drop annotations/pins wherever a user touches a certain location on the map. Whenever a user drops more 2 or more pins, it creates a polyline that connects the points. I got it to work in regular Swift 5, but I am trying to make it work in SwiftUI. I am using Mapbox. I am having trouble figuring out how to make it work with the coordinator. Can anyone help me figure this out? Thanks!
import SwiftUI
import Mapbox
// Creates an annotation using title & coordinate
extension MGLPointAnnotation {
convenience init(title: String, coordinate: CLLocationCoordinate2D) {
self.init()
self.title = title
self.coordinate = coordinate
}
}
// Represents & Displays an MGLMapView in SwiftUI
struct MapView: UIViewRepresentable {
// Property binding to add annotations
#Binding var annotations: [MGLPointAnnotation]
//var mapView: MGLMapView!
//var coordinates = [CLLocationCoordinate2D]()
// Creates a mapView with MGLMapView type
private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
// Needed function for UIViewRepresentable
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
// Add a single tap gesture recognizer. This gesture requires the built-in MGLMapView tap gestures (such as those for zoom and annotation selection) to fail.
let singleTap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleMapTap(sender:)))
for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
singleTap.require(toFail: recognizer)
}
mapView.addGestureRecognizer(singleTap)
// Convert `mapView.centerCoordinate` (CLLocationCoordinate2D) to screen location (CGPoint).
let centerScreenPoint: CGPoint = mapView.convert(mapView.centerCoordinate, toPointTo: nil)
print("Screen center: \(centerScreenPoint) = \(mapView.center)")
return mapView
}
// Needed function for UIViewRepresentable
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
updateAnnotations()
uiView.addAnnotations(annotations)
}
// Styles the map with a MapBox Studio URL
func styleURL(_ styleURL: URL) -> MapView {
mapView.styleURL = styleURL
return self
}
// Specifies where the map is centered
func centerCoordinate(_ centerCoordinate: CLLocationCoordinate2D) -> MapView {
mapView.centerCoordinate = centerCoordinate
return self
}
// Specifies the zoom level of the initial view
func zoomLevel(_ zoomLevel: Double) -> MapView {
mapView.zoomLevel = zoomLevel
return self
}
// Updates the annotations in the view
private func updateAnnotations() {
if let currentAnnotations = mapView.annotations {
mapView.removeAnnotations(currentAnnotations)
}
mapView.addAnnotations(annotations)
}
// Makes the coordinater (coordinator class below)
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self, mapView)
}
// A coordinator used with a delegate to add the annotation view to the map
// A coordinator class is declared to implement and view MGLMapViewDelegate in SwiftUI
final class Coordinator: NSObject, MGLMapViewDelegate {
var control: MapView
var mapView: MGLMapView!
var coordinates = [CLLocationCoordinate2D]()
var pointAnnotations = [MGLPointAnnotation]()
init(_ control: MapView, _ mapView: MGLMapView) {
self.control = control
self.mapView = mapView
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
return nil
}
#objc func handleMapTap(sender: UITapGestureRecognizer) {
// Convert tap location (CGPoint) to geographic coordinate (CLLocationCoordinate2D).
let tapPoint: CGPoint = sender.location(in: mapView)
let tapCoordinate: CLLocationCoordinate2D = mapView.convert(tapPoint, toCoordinateFrom: mapView)
print("You tapped at: \(tapCoordinate.latitude), \(tapCoordinate.longitude)")
// Create an array of coordinates for our polyline, starting at the center of the map and ending at the tap coordinate.
//var coordinates: [CLLocationCoordinate2D] = [mapView.centerCoordinate]
coordinates.append(tapCoordinate)
print("Coordinates list: \(coordinates)")
//var pointAnnotations = [MGLPointAnnotation]()
for coordinate in coordinates {
let point = MGLPointAnnotation()
point.coordinate = coordinate
point.title = "\(coordinate.latitude), \(coordinate.longitude)"
pointAnnotations.append(point)
print("Annotations: \(pointAnnotations)")
let polyline = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
mapView.addAnnotation(polyline)
}
mapView.addAnnotations(pointAnnotations)
// Remove any existing polyline(s) from the map.
//if mapView.annotations?.count != nil, let existingAnnotations = mapView.annotations {
//mapView.removeAnnotations(existingAnnotations)
//}
// Add a polyline with the new coordinates.
//let polyline = MGLPolyline(coordinates: &coordinates, count: UInt(coordinates.count))
//mapView.addAnnotation(polyline)
}
}
}
First of all, the single tap cannot work if making it 'to fail' for other recognisers, (I don't know why that happened in SwiftUI). Removing the for loop makes it work, but also disables other single tap features on the map, for example, annotations will not be clickable.
Therefore, I suggest using some other gesture instead, the following example is based on long press.
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
let longTap = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleMapTap(sender:)))
mapView.addGestureRecognizer(longTap)
return mapView
}
Second, the 'mapView' in the handler function should be 'control.mapView' according to the first line of your 'Coordinator' class.
#objc func handleMapTap(sender: UILongPressGestureRecognizer) {
// Run the code unless user end the long press (Long press handler keep calling during press)
//guard sender.state == .ended else { return }
// Convert tap location (CGPoint) to geographic coordinate (CLLocationCoordinate2D).
let locationInMap = sender.location(in: control.mapView)
let coordinateSet = control.mapView.convert(locationInMap, toCoordinateFrom: nil)
print("You tapped at: \(coordinateSet.latitude)")
if sender.state == UIGestureRecognizer.State.ended {
print("You final tapped at: \(coordinateSet.latitude)")
}
}
I faced with the same issue with you.
I updated my library to the newest version and every thing worked!

class extension not being called - unable to configure annotations on map

I'm on Swift 5 and Xcode 11, currently trying to create a map with predefined annotations using custom images instead of the default markers. I've been playing around with Apple's sample code for AnnotatingMapWithCustomData and could tailor it to what I needed. But once I tried to copy the code into my own project, the annotations showed up as default markers instead of the custom annotationViews I configured.
import UIKit
import MapKit
class MapViewController: UIViewController {
#IBOutlet private weak var mapView: MKMapView!
private var allAnnotations: [MKAnnotation]?
private var displayedAnnotations: [MKAnnotation]? {
willSet {
if let currentAnnotations = displayedAnnotations {
mapView.removeAnnotations(currentAnnotations)
}
}
didSet {
if let newAnnotations = displayedAnnotations {
mapView.addAnnotations(newAnnotations)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
registerMapAnnotationViews()
// Create the array of annotations and the specific annotations for the points of interest.
allAnnotations = [ShrekAnnotation(), CoffeeAnnotation()]
// Dispaly all annotations on the map.
displayedAnnotations = allAnnotations
centerMapOnLondon()
}
/// - Tag: RegisterAnnotationViews
private func registerMapAnnotationViews() {
mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(ShrekAnnotation.self))
mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(CoffeeAnnotation.self))
}
private func centerMapOnLondon() {
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let center = CLLocationCoordinate2D(latitude: 51.507911, longitude: -0.132222)
mapView.setRegion(MKCoordinateRegion(center: center, span: span), animated: true)
}
}
extension MapViewController: MKMapViewDelegate {
/// - Tag: CreateAnnotationViews
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !annotation.isKind(of: MKUserLocation.self) else {
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
return nil
}
var annotationView: MKAnnotationView?
if let annotation = annotation as? ShrekAnnotation {
annotationView = setupShrekAnnotationView(for: annotation, on: mapView)
} else if let annotation = annotation as? CoffeeAnnotation {
annotationView = setupCoffeeAnnotationView(for: annotation, on: mapView)
}
return annotationView
}
/// - Tag: ConfigureAnnotationViews
private func setupShrekAnnotationView(for annotation: ShrekAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(ShrekAnnotation.self)
let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
view.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "map-shrek1")
view.image = image
return view
}
/// - Tag: ConfigureAnnotationViews
private func setupCoffeeAnnotationView(for annotation: CoffeeAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(CoffeeAnnotation.self)
let view = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
view.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "map-cof1")
view.image = image
return view
}
}
Apparently the class extension is not being called but I don't know how to fix this. The code that worked before I tried implementing it in my own project is identical. Could I have missed something I should've copied too or can you find the reason for the bug in the code?
Thanks for any help.
I think you are not setting the mapView delegate hence viewFor annotation is not being triggered and your custom annotation view is not appearing on map. Set
mapView.delegate = self
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
registerMapAnnotationViews()
// Create the array of annotations and the specific annotations for the points of interest.
allAnnotations = [ShrekAnnotation(), CoffeeAnnotation()]
// Dispaly all annotations on the map.
displayedAnnotations = allAnnotations
centerMapOnLondon()
}
Hope this helps

Changing the value of a BindableObject passed into a "MapView: UIViewRepresentable" when calloutAccessoryControlTapped

Building a mostly SwiftUI app... I'm trying to connect (bind) the value of a BindableObject in a parent view to a MapKit child view, such that when the annotation (callout accessory) is tapped (the little (i) button on the annotation label...
...it changes the value of $showDetails to true, which is wired up to a "sheet(isPresented: $showDetails...)" further up the view hierarchy, which then displays a modal:
struct MapView: UIViewRepresentable {
#Binding var mapSelected: Int
#Binding var showDetails: Bool // I WANT TO TOGGLE THIS WHEN CALLOUT ACCESSORY IS TAPPED
#Binding var location: Location
#Binding var previousLocation: Location
#Binding var autoZoom: Bool
#Binding var autoZoomLevel: Int
class Coordinator: NSObject, MKMapViewDelegate {
#Binding var showDetails: Bool
init(showDetails: Binding<Bool>) {
_showDetails = showDetails
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let coordinates = view.annotation?.coordinate else { return }
let span = mapView.region.span
let region = MKCoordinateRegion(center: coordinates, span: span)
mapView.setRegion(region, animated: true)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? LocationAnnotation else { return nil }
let identifier = "Annotation"
var annotationView: MKMarkerAnnotationView? = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView
if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView?.markerTintColor = UIColor(hex: "#00b4ffff")
annotationView?.animatesWhenAdded = true
annotationView?.canShowCallout = true
annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
} else {
annotationView?.annotation = annotation
}
return annotationView
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
guard let loc = view.annotation as? LocationAnnotation else {
print("sorry")
return
}
print(self.showDetails) // true, false, true, false, ...
self.showDetails.toggle() // THIS DOES TOGGLE, BUT THE VALUE IS NOT OBSERVED BY THE PARENT VIEW
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(showDetails: $showDetails)
}
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
return map
}
func updateUIView(_ uiView: MKMapView, context: Context) {
switch mapSelected {
case 0:
uiView.mapType = .standard
default:
uiView.mapType = .hybrid
}
let currentRegion = uiView.region // get the current region
var span: MKCoordinateSpan
var center: CLLocationCoordinate2D
var newRegion: MKCoordinateRegion
if currentRegion.span.latitudeDelta == 90.0 && currentRegion.span.longitudeDelta == 180.0 { // INITIAL
span = MKCoordinateSpan(latitudeDelta: 18.0, longitudeDelta: 18.0)
center = CLLocationCoordinate2D(latitude: 54.5, longitude: -110)
newRegion = MKCoordinateRegion(center: center, span: span)
uiView.setRegion(newRegion, animated: true)
}
updateAnnotations(from: uiView)
}
// ...
}
What I expect is that when the callout accessory is tapped, showDetails is toggled (which it is), but no effect is seen in the parent view—the sheet is not presented. Seems that the binding is not publishing its new state.
What am I missing / doing wrong? I find integrating UIKit with SwiftUI to be easy, then hard, then easy, then impossible. Help please!
As it turns out, I was looking in the wrong place. The problem lay not in the above code (which is 100% fine), but rather in a container view that should have been listening to the changed value of showDetails, but wasn't because of the way I passed showDetails into it, e.g.,
ContentView.swift
Footer(search: search,
locationStore: self.locationStore,
searchCoordinates: self.searchCoordinates,
showDetails: self.showDetails) // NOT PASSED AS A BINDING...
Footer.swift
struct Footer: View {
#EnvironmentObject var settingsStore: SettingsStore
#ObservedObject var search: SearchTerm
#ObservedObject var locationStore: LocationStore
#ObservedObject var searchCoordinates: SearchCoordinates
#State var showDetails: Bool // DECLARE LOCAL STATE, WILL NOT BE AWARE OF CHANGE TO showDetails FROM MapView
Very simple fix:
ContentView.swift
Footer(search: search,
locationStore: self.locationStore,
searchCoordinates: self.searchCoordinates,
showDetails: self.$showDetails)
Footer.swift
struct Footer: View {
#EnvironmentObject var settingsStore: SettingsStore
#ObservedObject var search: SearchTerm
#ObservedObject var locationStore: LocationStore
#ObservedObject var searchCoordinates: SearchCoordinates
#Binding var showDetails: Bool
It feels like bugs like this one are pretty easy to encounter in SwiftUI (which is awesome, btw) when accidentally using the wrong property wrapper on passed-in variables. The compiler won't warn you and there are no runtime errors, just... nothing happens. And you, like me, may be inclined to chase red herrings a.k.a. perfectly fine code.

Accessing MKMapView elements as UIViewRepresentable in the main (ContentView) SwiftUI view

I am using SwiftUI to display a map and if user tapped on an annotation, it pops up a detail view in the VStack. I have made the map view and inserted annotations in another SwiftUI file. I also made the detail view.
How can I access the annotations of that map in the main view file to define a .tapaction for them to use it for the detailed view?
I tried defining the view as MKMapView but it is not possible to do it for a UIViewRepresentable inside another SwiftUI view.
The main view (ContentView) code is:
struct ContentView: View {
#State private var chosen = false
var body: some View {
VStack {
MapView()
.edgesIgnoringSafeArea(.top)
.frame(height: chosen ? 600:nil)
.tapAction {
withAnimation{ self.chosen.toggle()}
}
if chosen {
ExtractedView()
}
}
}
}
The MapView code is:
struct MapView : UIViewRepresentable {
#State private var userLocationIsEnabled = false
var locationManager = CLLocationManager()
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
view.showsUserLocation = true
.
.
.
let sampleCoordinates = [
CLLocation(latitude: xx.xxx, longitude: xx.xxx),
CLLocation(latitude: xx.xxx, longitude: xx.xxx),
CLLocation(latitude: xx.xxx, longitude: xx.xxx)
]
addAnnotations(coords: sampleCoordinates, view: view)
}
}
}
I expect to be able to access map view annotations and define tapaction in another view.
In SwiftUI DSL you don't access views.
Instead, you combine "representations" of them to create views.
A pin can be represented by an object - manipulating the pin will also update the map.
This is our pin object:
class MapPin: NSObject, MKAnnotation {
let coordinate: CLLocationCoordinate2D
let title: String?
let subtitle: String?
let action: (() -> Void)?
init(coordinate: CLLocationCoordinate2D,
title: String? = nil,
subtitle: String? = nil,
action: (() -> Void)? = nil) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.action = action
}
}
Here's my Map, which is not just UIViewRepresentable, but also makes use of a Coordinator.
(More about UIViewRepresentable and coordinators can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)
struct Map : UIViewRepresentable {
class Coordinator: NSObject, MKMapViewDelegate {
#Binding var selectedPin: MapPin?
init(selectedPin: Binding<MapPin?>) {
_selectedPin = selectedPin
}
func mapView(_ mapView: MKMapView,
didSelect view: MKAnnotationView) {
guard let pin = view.annotation as? MapPin else {
return
}
pin.action?()
selectedPin = pin
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard (view.annotation as? MapPin) != nil else {
return
}
selectedPin = nil
}
}
#Binding var pins: [MapPin]
#Binding var selectedPin: MapPin?
func makeCoordinator() -> Coordinator {
return Coordinator(selectedPin: $selectedPin)
}
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotations(pins)
if let selectedPin = selectedPin {
uiView.selectAnnotation(selectedPin, animated: false)
}
}
}
The idea is:
The pins are a #State on the view containing the map, and are passed down as a binding.
Each time a pin is added or removed, it will trigger a UI update - all the pins will be removed, then added again (not very efficient, but that's beyond the scope of this answer)
The Coordinator is the map delegate - I can retrieve the touched MapPin from the delegate methods.
To test it:
struct ContentView: View {
#State var pins: [MapPin] = [
MapPin(coordinate: CLLocationCoordinate2D(latitude: 51.509865,
longitude: -0.118092),
title: "London",
subtitle: "Big Smoke",
action: { print("Hey mate!") } )
]
#State var selectedPin: MapPin?
var body: some View {
NavigationView {
VStack {
Map(pins: $pins, selectedPin: $selectedPin)
.frame(width: 300, height: 300)
if selectedPin != nil {
Text(verbatim: "Welcome to \(selectedPin?.title ?? "???")!")
}
}
}
}
}
...and try zooming/tapping the pin on London, UK :)