Dropping annotation pins

Dropping annotation pins 📌

Pins on a map are represented by MKPointAnnotation objects. Below the ContentView‘s locationManager property, we add such an object as a State property. Since the map will not have a pin annotation at launch, we define the State as an Optional. Because an MKPointAnnotation object is part of the MapKit framework, we have to import it into our ContentView.swift file.

import MapKit

struct ContentView: View {
    
    @State var currentAnnotation: MKPointAnnotation?
    
    //...
    
    var body: some View {
        //...
    }
}

In our MyMap, we now declare a corresponding Binding.

@Binding var currentAnnotation: MKPointAnnotation?

We assign this Binding to the currentAnnotation State of our ContentView:

MyMap(currentRegion: $locationManager.currentRegion, currentAnnotation: $currentAnnotation)

When we run the app, our map does not show any annotation yet. We want the user to be able to create one by dropping a pin on the map. 

A pin should be dropped when the user long-presses on a specific point on the map. For our MyMap to recognize this, we need to add a gesture recognizer to the makeUIView function. To do this, we create a UILongGestureRecognizer instance that calls a didLongPress method inside the Coordinator subclass of our MyMap (we will add this method in a moment).

func makeUIView(context: Context) -> MKMapView {
    //...
        
    let longPress = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.didLongPress(gesture:)))
        
    return map
}

The user needs to press on the screen for at least one second to drop a pin. 

func makeUIView(context: Context) -> MKMapView {
    //...
        
    let longPress = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.didLongPress(gesture:)))
    longPress.minimumPressDuration = 1.0
        
    return map
}

Finally, we add the created recognizer to our actual map:

func makeUIView(context: Context) -> MKMapView {
    //...
    
    let longPress = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.didLongPress(gesture:)))
    longPress.minimumPressDuration = 1.0
    map.addGestureRecognizer(longPress)
    
    return map
}

Next, we add the didLongPress method to our Coordinator subclass. 


class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
    
    //...
    
    @objc func didLongPress(gesture: UITapGestureRecognizer) {
        
    }
    
    
}

In this method, we write the code needed to drop a pin at the selected location. To avoid that the code gets permanently executed while the user is still pressing and instead only once after the one second has elapsed, we write:

@objc func didLongPress(gesture: UITapGestureRecognizer) {
    if gesture.state == UIGestureRecognizer.State.began    {
        //Drop Pin Annotation
    } else {
        return
    }
}

Inside the if-block, we need to know which point of the screen the user has been long-pressed. 

@objc func didLongPress(gesture: UITapGestureRecognizer) {
    if gesture.state == UIGestureRecognizer.State.began    {
        let touchedPoint = gesture.location(in: gesture.view)
    } else {
        return
    }
}

But this information alone is not enough since we do not know which coordinates the user did select on the map. Fortunately, the selected point on the device’s screen can be easily converted into the corresponding coordinates on the map. However, our coordinator needs access to the map we create within the makeUIView function. Therefore, we add an optional map property to the Coordinator subclass.

var map: MKMapView?

Within our makeUIView function, we can now assign the created map to the optional property. 

func makeUIView(context: Context) -> MKMapView {
    //...
    
    context.coordinator.map = map
    
    return map
}

Now that our coordinator has access to the created map, we can convert the touched point to the corresponding coordinates by writing: 

@objc func didLongPress(gesture: UITapGestureRecognizer) {
    if gesture.state == UIGestureRecognizer.State.began    {
        let touchedPoint = gesture.location(in: gesture.view)
        guard let touchedCoordinates = map?.convert(touchedPoint, toCoordinateFrom: map) else {return}
    } else {
        return
    }
}

By using the guard statement, we make sure that this succeeded before continuing. 

By using these coordinates, we can now create a corresponding MKPointAnnotation. Then, we assign the new annotation to the currentAnnotation Binding of our MyMap

@objc func didLongPress(gesture: UITapGestureRecognizer) {
    if gesture.state == UIGestureRecognizer.State.began    {
        let touchedPoint = gesture.location(in: gesture.view)
        guard let touchedCoordinates = map?.convert(touchedPoint, toCoordinateFrom: map) else {return}
        let newAnnotation = MKPointAnnotation()
        newAnnotation.title = "Tap to see photos"
        newAnnotation.coordinate = CLLocationCoordinate2D(latitude: touchedCoordinates.latitude, longitude: touchedCoordinates.longitude)
        parent.currentAnnotation = newAnnotation
    } else {
        return
    }
}

This causes the updateUIView function to be triggered again. Therefore, we write inside it that the currentAnnotation should be added to the map. To make sure that the Optional does not contain nil, we use a corresponding if statement.

func updateUIView(_ map: MKMapView, context: Context) {
    
    if currentAnnotation != nil {
        map.removeAnnotations(map.annotations)
        map.addAnnotation(currentAnnotation!)
    }
    
    //...
}

Note: We use the removeAnnotations method to ensure that the user cannot create more than one annotation at a time.

Let’s run the app to see if that works! Awesome, when we long-press on the screen, the didLongPress method gets triggered. It converts the touched point into geographic coordinates and updates the currentAnnotation Binding accordingly. In turn, this triggers the updateUIView function of our MyMap where we use the new currentAnnotation to add it to our map. If we long-press at another position, the old annotation gets removed before the new one gets added!

Finally, we want the map to navigate to the dropped pin. For this purpose, we update the currentRegion Binding of the MyMap in the didLongPress method of our Coordinator.

@objc func didLongPress(gesture: UITapGestureRecognizer) {
    if gesture.state == UIGestureRecognizer.State.began    {
        //...
        parent.currentRegion = MKCoordinateRegion(center: newAnnotation.coordinate, latitudinalMeters: 1000, longitudinalMeters: 1000)
    } else {
        return
    }
}

If we rerun the app and drop a new pin, the map will move accordingly.

But how does the user of the app know that he is supposed to long-press to drop a pin? For this purpose, we go to our ContentView and wrap the whole ZStack into a NavigationView and add a suitable .navigationBarTitle:

NavigationView {
    ZStack(alignment: .bottomTrailing) {
        //...
    }
        .navigationBarTitle("Long-press to drop pin", displayMode: .inline)
        .edgesIgnoringSafeArea(.all)
}

Now the user knows how to drop a pin at any location. The next thing we want to do is to open a modal view when the user taps on this pin. In this modal view, we will display the corresponding photos later on.

2 replies on “Dropping annotation pins”

Hello Fellow BlackBirds,
unfortunately the updateUIView with map: MKMapView does not seem to work in iOS Version 14.3 anymore. However, you can make it work if you cast the UIViewType into a MKMapView! as follows: “let map uiView as! MkMapView”
In my test it did work without any issue. It might be helpful for a few others out there.

Leave a Reply

Your email address will not be published. Required fields are marked *