I have a MKMapView with several different types of MKAnnotationView
s on it. Chiefly, I have a PricedAnnotationView
, which is a fairly complicated MKAnnotationView
with a UILabel
and some other views. The label can have different text in it, based off of the original MKAnnotation
that it is associated with.
class PricedAnnotationView: MKAnnotationView {
static let ReuseID = "pricedAnnotation"
let labelContainingView: UIView = {
let containingView = UIView()
containingView.backgroundColor = .blue
containingView.layer.cornerRadius = 3
containingView.translatesAutoresizingMaskIntoConstraints = false
return containingView
}()
let label: UILabel = {
let label = UILabel(frame: CGRect.zero)
label.textColor = .white
label.font = ViewConstants.Text.BodyFontBold
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let containingView: UIView = {
let v = UIView(frame:CGRect.zero)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
let triangle = UIView(frame: CGRect(x: 0, y: 0, width: 9, height: 9))
triangle.translatesAutoresizingMaskIntoConstraints = false
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 5.5, y: 11))
path.addLine(to: CGPoint(x: 11, y: 0))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
triangleLayer.fillColor = UIColor.blue.cgColor
triangle.layer.addSublayer(triangleLayer)
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.canShowCallout = false
self.frame = labelContainingView.frame
labelContainingView.addSubview(label)
containingView.addSubview(labelContainingView)
containingView.addSubview(triangle)
self.addSubview(containingView)
// Get the label correctly inset in the labelContainingView
label.topAnchor.constraint(equalTo: labelContainingView.topAnchor, constant: 3).isActive = true
label.leadingAnchor.constraint(equalTo: labelContainingView.leadingAnchor, constant: 6).isActive = true
label.trailingAnchor.constraint(equalTo: labelContainingView.trailingAnchor, constant: -6).isActive = true
label.bottomAnchor.constraint(equalTo: labelContainingView.bottomAnchor, constant: -3).isActive = true
// The triangle.topAnchor purposefully puts the triangle a bit under the label. In testing, while moving around
// the map, a little gap would appear between the label and the triangle. This change fixes that. The triangle
// was made two pixels bigger so that it would appear to be the same size.
triangle.topAnchor.constraint(equalTo: labelContainingView.bottomAnchor, constant: -2).isActive = true
triangle.centerXAnchor.constraint(equalTo: labelContainingView.centerXAnchor).isActive = true
triangle.widthAnchor.constraint(equalToConstant: 11).isActive = true
triangle.heightAnchor.constraint(equalToConstant: 11).isActive = true
containingView.topAnchor.constraint(equalTo: labelContainingView.topAnchor).isActive = true
containingView.leadingAnchor.constraint(equalTo: labelContainingView.leadingAnchor).isActive = true
containingView.trailingAnchor.constraint(equalTo: labelContainingView.trailingAnchor).isActive = true
containingView.bottomAnchor.constraint(equalTo: triangle.bottomAnchor).isActive = true
}
/// - Tag: DisplayConfiguration
override func prepareForDisplay() {
super.prepareForDisplay()
displayPriority = .required
guard let annotation = annotation as? MyAnnotation else { return }
if case let .priced(price, currencyCode) = annotation.status {
label.text = StringFormatter.formatCurrency(amount: price, currencyCode: currencyCode)
}
self.layoutIfNeeded() // Calculate the size from the constraints so we can know the frame.
self.frame = containingView.frame
self.centerOffset = CGPoint(x: 0, y: -(containingView.frame.height / 2)) // Center point should be where the triangle points
}
}
Every other annotation is much simpler and just uses one of two UIImage
to support itself. I use two different reuse Ids with those simpler MKAnnotationView
so I don't have to keep setting the image; I don't know if that is a best practice or not. Here is a sample of how I do it:
private let unpricedAnnotationClusterId = "unpriced"
private let unpricedAnnotationReuseID = "unpricedAnnotation"
func createUnpricedAnnotation(mapView: MKMapView, annotation: MKAnnotation) -> MKAnnotationView {
let annotationView: MKAnnotationView
if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: unpricedAnnotationReuseID) {
annotationView = dequeuedAnnotationView
annotationView.annotation = annotation
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: unpricedAnnotationReuseID)
annotationView.image = UIImage(bundleAsset: "map_pin_blue")
annotationView.centerOffset = CGPoint(x: 0, y: -(annotationView.frame.height / 2)) // Center point should be where the pin points
annotationView.clusteringIdentifier = unpricedAnnotationClusterId
}
return annotationView
}
When the user taps on an annotation in the map, I have an information window below the map populate with information about the selected item. That works fine. I would like it if the annotation view changed though so it was clear as to where the item is that is being displayed. I would be okay with just setting it to an image. However, I can't find any example of how to do that; most examples just change the image. When I try that with PricedAnnotationView
the previous label does not go away, although the two simpler annotations work fine. I'm also unsure how changing the image would affect the reuse Ids that I am using. I don't see a way to manually change the reuse identifier.
My MKMapViewDelegate
has these functions:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if allowSelection {
if let item : MyAnnotation = view.annotation as? MyAnnotation {
...
// display selected item in information view
...
}
view.image = UIImage(bundleAsset: "map_pin_red")
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MyAnnotation else {
return nil
}
switch annotation.status {
case .notPriced:
return createUnpricedAnnotation(mapView: mapView, annotation: annotation)
case .priced(_, _):
return PricedAnnotationView(annotation: annotation, reuseIdentifier: PricedAnnotationView.ReuseID)
default:
return createErrorAnnotation(mapView: mapView, annotation: annotation)
}
}
If I could go the route of just setting the image, how do I change it back after the user has selected something else. Would I need to recreate PricedAnnotationView
from scratch if it was previously one of those annotation? What would that do to the reuse Ids and the reuse queue?
When I have just changed the image, the view for PricedAnnotationView
does not actually go away, and is just moved to the side as the image is shown beneath.
Ideally, I could do something to trigger a call to mapView(_:viewFor:)
and have some intelligence in there to remember if the item is selected or now, but I have not found anything in my research to show how to do that. (It is not completely trivial because the isSelected
property does not seem to be set on the MKAnnotationView
that I would have just dequeued. No surprise there.)
Any suggestions as to how to handle this would be greatly appreciated.