iosmkmapviewmkpinannotationviewmkmapviewdelegatemkpointannotation

How to update information on MKPinAnnotationView?


I've had some past experience using MKMapView and MKPointAnnotation, which I used to put some pin on a map. This time I am trying to go one step further and use MKPinAnnotationView, to write a label along with some of the pins.

Unfortunately, it doesn't all work as I expect.

Here is what I want to do:

I have a map (an MKMapView object) and when I touch it, I put a pin at the touch point, then some computation is performed and this gives me a second point on the map. I put a second pin on the map (located at the second point), on this last pin I want to put a label, say "Hello Second!", but this label needs to be updated when the pin changes place.

Here is the relevant code:

class ViewController: UIViewController, MKMapViewDelegate {
    var mapView:MKMapView!, touchPoint,secondPoint:MKPointAnnotation!

    override func viewDidLoad() {
        super.viewDidLoad()
        mapView = MKMapView()
        ...........
        let mapTap = UITapGestureRecognizer(target: self,
                                            action: #selector(ViewController.mapTouchHandler))
        mapView.addGestureRecognizer(mapTap)
    }


    func mapTouchHandler(gesture:UITapGestureRecognizer) {
        ...........
        // Compute map coordinates for the touch point (tapGeoPoint).

        if touchPoint == nil {
            touchPoint = MKPointAnnotation()
            mapView.addAnnotation(touchPoint);
        }

        touchPoint.coordinate = CLLocationCoordinate2D(latitude: tapGeoPoint.latitude,
                                                       longitude: tapGeoPoint.longitude)
        ...........
        computeSecondPoint(url: someComputedURL)
    }


    func computeSecondPoint(url searchURL:String) {
        let reqURL = NSURL(string: searchURL)!, session = URLSession.shared,
        task = session.dataTask(with: reqURL as URL) {
            (data: Data?, response: URLResponse?, error: Error?) in
            if error == nil {
                do {let allData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSArray
                    .................
                    // Compute map coordinates for the second point (secondPointCoord).

                    if self.secondPoint == nil {
                        self.secondPoint = MKPointAnnotation()
                        self.mapView.addAnnotation(self.secondPoint)
                    }

                    DispatchQueue.main.async {
                        () -> Void in
                        self.secondPoint.coordinate = CLLocationCoordinate2D(latitude: secondPointCoord.latitude,
                                                                             longitude: secondPointCoord.longitude)
                        self.secondPoint.title = "Hello Second -TITLE!"
                        //* I want to update the label for this pin (attached to the secondPoint) too.
                    }
                } catch let error as NSError {print(error.localizedDescription)}
            } else {
                print("Error inside \(#function):\n\(error)")
            }
        }

        task.resume()

    }


    func mapView(_ mapView: MKMapView,
                 viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let identifier = "pin"
        var view: MyPinAnnotationView
        if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
            as? MyPinAnnotationView {
            dequeuedView.annotation = annotation
            view = dequeuedView
        } else {
            view = MyPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)

            if ((annotation.coordinate.latitude != touchPoint.coordinate.latitude) ||
                (annotation.coordinate.longitude != touchPoint.coordinate.longitude)) {//* I need a better test to check that this not touchPoint!
                view.pinTintColor = UIColor.blue
                view.canShowCallout = true
                view.calloutOffset = CGPoint(x: -7, y: 0)
                view.setInfo(title: "Hi Start Label!")
            } else {
                view.pinTintColor = UIColor.red
                view.canShowCallout = false
            }
        }
        return view
    }
}

Here is the class MyPinAnnotationView:

import UIKit
import MapKit

class MyPinAnnotationView: MKPinAnnotationView {
    let information:UILabel = UILabel(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 70.0, height: 30.0)))

    func setInfo(title : String)
    {
        information.text = title
        information.textAlignment = .center
        self.addSubview(information)
    }


    func hideInfo() {
        information.removeFromSuperview()
    }
}

The lines with a comment marked //*, show where I need some help.

Now the label appears as first set, but I don't know how to update it.


Solution

  • If you want the annotation view to show some text that is updated as the annotation changes, use KVO on the annotation. So, first, create a model object, the annotation, which includes the new property to be observed:

    class MyAnnotation: NSObject, MKAnnotation {
        dynamic var title: String?
        dynamic var subtitle: String?
        dynamic var coordinate: CLLocationCoordinate2D
        dynamic var information: String?
    
        init(title: String? = nil, subtitle: String? = nil, coordinate: CLLocationCoordinate2D, information: String? = nil) {
            self.title = title
            self.subtitle = subtitle
            self.coordinate = coordinate
            self.information = information
        }
    }
    

    I've called this new property information, but you can probably come up with a better name that captures the functional intent of this property. But hopefully it illustrates the idea. The key takeaway here is that if this property may change at a later point, we'll want to make it dynamic so we can use KVO to observe those changes.

    class MyPinAnnotationView: MKPinAnnotationView {
        private let informationLabel = UILabel(frame: CGRect(origin: .zero, size: CGSize(width: 70.0, height: 30.0)))
    
        private var observerContext = 0
    
        override var annotation: MKAnnotation? {
            willSet {
                removeObserverIfAny()
            }
            didSet {
                if let annotation = annotation as? MyAnnotation {
                    annotation.addObserver(self, forKeyPath: #keyPath(MyAnnotation.information), context: &observerContext)
                    informationLabel.text = annotation.information
                }
            }
        }
    
        deinit {
            removeObserverIfAny()
        }
    
        private func removeObserverIfAny() {
            if let oldAnnotation = annotation as? MyAnnotation {
                oldAnnotation.removeObserver(self, forKeyPath: #keyPath(MyAnnotation.information))
            }
        }
    
        func showInformation() {
            addSubview(informationLabel)
        }
    
        func hideInformation() {
            informationLabel.removeFromSuperview()
        }
    
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            guard context == &observerContext else {
                super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
                return
            }
    
            if let annotation = annotation as? MyAnnotation, let information = annotation.information {
                informationLabel.text = information
            }
        }
    
    }
    
    extension ViewController: MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            if annotation is MKUserLocation { return nil }
    
            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyPinAnnotationView
            if annotationView == nil {
                annotationView = MyPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
                annotationView?.canShowCallout = true
            } else {
                annotationView?.annotation = annotation
            }
            annotationView?.showInformation()
            return annotationView
        }
    }
    

    I've changed the name of the label to informationLabel to make it a little more explicit that it is a view, not to be confused with the model property, information, of our new MyAnnotation class. Also, I'd suggest more meaningful class names than MyAnnotation and MyPinAnnotationView, perhaps using some name that better captures the functional intent of these two classes.

    Regardless, as you can see, when you set the annotation for this annotation view, it updates the label text. But it also observes the annotation's new information property via KVO, so if this property changes later, the view will update accordingly.