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!
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.