4

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.

  • First issue, I want to update the label on the secondPoint, but I don't know what code to use. Line:

    //* I want to update the label for this pin (attached to the secondPoint) too.

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

  • Second issue, there must be a better way to test which pin I am dealing with. Line:

    //* I need a better test to check that this not touchPoint!

Michel
  • 10,303
  • 17
  • 82
  • 179
  • Will you share your demo, if you dont mind ? I can understand your issue and post answer of it – Jitendra Modi Dec 24 '16 at 08:45
  • Fine, but let me know what you need to know that is not in my post. I have already shared all I can think of on the subject, here in the post. – Michel Dec 24 '16 at 09:15
  • check this answer I think that will help you http://stackoverflow.com/questions/24467408/swift-add-mkannotationview-to-mkmapview – Jitendra Modi Dec 24 '16 at 09:17
  • Hum? But I can add an MKAnnotationView. My problem is to update the contents of the label I added, when I want. – Michel Dec 24 '16 at 09:23
  • means if you click somewhere else then you have to update your label with ? – Jitendra Modi Dec 24 '16 at 09:24
  • You see the line: "self.secondPoint.title = "Hello Second -TITLE!"" ? This is where I want to update the label I added (probaly using setInfo()), but I don't know how to do it. – Michel Dec 24 '16 at 09:28
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/131402/discussion-between-jecky-and-michel). – Jitendra Modi Dec 24 '16 at 09:32

1 Answers1

3

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • No, I don't want to observe title, because of the reason you mentioned. But I will keep in mind your observer idea and see if I can do something with it. I want to update the information UILabel (See my post). – Michel Dec 24 '16 at 12:42
  • I certainly need to work, starting with your idea. I have to say, just by reading your code I don't understand much at this point. Eventually I want to update the contents of information(UILabel), when the user touches the display and secondPoint changes place. I hope what you suggest will enable me to do that, once I see how it works. – Michel Dec 24 '16 at 15:38
  • @Michel - Yeah, that's why we use observer pattern, so that you can update some annotation property at a later point and the annotation view will update accordingly. Just observe some property other than `title`. See my expanded answer above. – Rob Dec 24 '16 at 16:16
  • It was quite a bit of work but it's OK now. Thanks a lot. – Michel Dec 25 '16 at 11:35
  • Works like a charm! – Itapox Aug 20 '20 at 23:36