-3

I have a MKMapView with several different types of MKAnnotationViews 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.

enter image description here

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.

Erik Allen
  • 1,841
  • 2
  • 20
  • 36
  • Why don't you show us your current code for what you do have, and maybe some screenshots of the behavior you're experiencing so that we could get a better idea of what you're dealing with? – Pierce Jan 04 '20 at 03:29

1 Answers1

1

I've played around with this, and here is what I've done to get around this issue:

I stopped using different reuse ids for the two types of image annotations. I created this function to manage that:

func createImageAnnotation(mapView: MKMapView, annotation: MKAnnotation, imageString: String) -> MKAnnotationView {
    let annotationView: MKAnnotationView
    if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: imageAnnotationReuseID) {
        annotationView = dequeuedAnnotationView
        annotationView.annotation = annotation
    } else {
        annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: imageAnnotationReuseID)
    }
    annotationView.image = UIImage(bundleAsset: imageString)
    annotationView.centerOffset = CGPoint(x: 0, y: -(annotationView.frame.height / 2))    // Center point should be where the pin points
    annotationView.clusteringIdentifier = imageAnnotationClusterId
    return annotationView
}

I then added to the select and deselect calls so that the PricedAnnotationView is hidden and the image set when appropriate. Like this:

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: selectedAnnotationImage)
            if case .priced(_,_) = item.rateStatus {
                (view as! PricedAnnotationView).containingView.isHidden = true
            }
            view.centerOffset = CGPoint(x: 0, y: -(view.frame.height / 2))    // Center point should be where the pin points
        }
    }
}

func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
    guard let annotation = view.annotation as? MyAnnotation else { return }
    switch annotation.rateStatus {
    case .notPriced:
        view.image = UIImage(bundleAsset: unpricedAnnotationImage)
    case .priced(_, _):
        view.image = nil
        if let myView = (view as? PricedAnnotationView) {
            myView.containingView.isHidden = false
            view.frame =  myView.containingView.frame
            view.centerOffset = CGPoint(x: 0, y: -(myView.containingView.frame.height / 2))    // Center point should be where the triangle points
        }
    default:
        view.image = UIImage(bundleAsset: errorAnnotationImage)
    }
}

This seems to mostly work, with the only quirk being that when the PricedAnnotationView is changed to the image, the image briefly and visibly shrinks from the old frame size to the correct frame size. However, if I do not set the frame, then the PricedAnnotationView is oddly offset.

Erik Allen
  • 1,841
  • 2
  • 20
  • 36