3

I am trying to change the image that is inside the MKAnnotation without removing the rounded shape.

Here I create a custom class of MKAnnotation:

class MapPin: NSObject, MKAnnotation {
    let title: String?
    let locationName: String
    let coordinate: CLLocationCoordinate2D
    init(title: String, locationName: String, coordinate: CLLocationCoordinate2D) {
        self.title = title
        self.locationName = locationName
        self.coordinate = coordinate
    }
}

Here I create a MapPin and I add it to the mapView

func setPinUsingMKAnnotation() {
   let pin1 = MapPin(title: "Here", locationName: "Device Location", coordinate: CLLocationCoordinate2D(latitude: 21.283921, longitude: -157.831661))
   let coordinateRegion = MKCoordinateRegion(center: pin1.coordinate, latitudinalMeters: 800, longitudinalMeters: 800)
   mapView.setRegion(coordinateRegion, animated: true)
   mapView.addAnnotations([pin1])
}

The first image is what I created, the second image is what I would like it to be.

enter image description here

I even implemented MKMapViewDelegate:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    var annotationView = MKAnnotationView()
    annotationView.image = #imageLiteral(resourceName: "heart")
    return annotationView
}

This is the result:

enter image description here

The rounded shape disappears.

I saw many tutorials about how to custom a pin, but they only explained how to put an image instead of the pin (like the hearth image above). I would like to know how to change the image (and color) of the pin and keep the rounded shape (see the blue pin image above).

Any hints? Thanks

Edoardo
  • 657
  • 7
  • 24
  • FWIW, I might suggest using `MKPointAnnotation` rather than building your own `MKAnnotation`. Or, if you definitely need some additional properties, then fine, subclass `MKPointAnnotation` rather than starting from scratch. There's no point in reinventing the wheel (plus `MKPointAnnotation` makes these `title` and `coordinate` properties `dynamic`, which is useful). – Rob Jul 22 '20 at 05:41

1 Answers1

7

If you want that rounded border, you can render it yourself, or easier, subclass MKMarkerAnnotationView rather than MKAnnotationView:

class CustomAnnotationView: MKMarkerAnnotationView {
    override var annotation: MKAnnotation? {
        didSet { configure(for: annotation) }
    }

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        glyphImage = ...
        markerTintColor = ...
            
        configure(for: annotation)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(for annotation: MKAnnotation?) {
        displayPriority = .required

        // if doing clustering, also add
        // clusteringIdentifier = ...
    }
}

That way, not only do you get the circular border, but you get all of the marker annotation view behaviors (shows the title of the annotation view below the marker, if you select on the marker annotation view, it becomes larger, etc.). There’s a lot of marker annotation view behaviors that you probably don’t want to have to write from scratch if you don’t have to. By subclassing MKMarkerAnnotationView instead of the vanilla MKAnnotationView, you get all those behaviors for free.

For example, you could:

class CustomAnnotationView: MKMarkerAnnotationView {
    static let glyphImage: UIImage = {
        let rect = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))
        return UIGraphicsImageRenderer(bounds: rect).image { _ in
            let radius: CGFloat = 11
            let offset: CGFloat = 7
            let insetY: CGFloat = 5
            let center = CGPoint(x: rect.midX, y: rect.maxY - radius - insetY)
            let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi, clockwise: true)
            path.addQuadCurve(to: CGPoint(x: rect.midX, y: rect.minY + insetY), controlPoint: CGPoint(x: rect.midX - radius, y: center.y - offset))
            path.addQuadCurve(to: CGPoint(x: rect.midX + radius, y: center.y), controlPoint: CGPoint(x: rect.midX + radius, y: center.y - offset))
            path.close()
            UIColor.white.setFill()
            path.fill()
        }
    }()

    override var annotation: MKAnnotation? {
        didSet { configure(for: annotation) }
    }

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        glyphImage = Self.glyphImage
        markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)

        configure(for: annotation)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(for annotation: MKAnnotation?) {
        displayPriority = .required

        // if doing clustering, also add
        // clusteringIdentifier = ...
    }
}

That yields:

enter image description here

Obviously, when you set glyphImage, set it to whatever image you want. The old SF Symbols doesn't have that “drop” image (though iOS 14 has drop.fill). But supply whatever 40 × 40 pt image view you want. I'm rendering it myself, but you can use whatever appropriately sized image from your asset catalog (or from the system symbols) that you want.


As an aside, since iOS 11, you wouldn't generally wouldn't implement mapView(_:viewFor:) at all, unless absolutely necessary (which it isn't in this case). For example, you can get rid of your viewFor method and just register your custom annotation view in viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()

    mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

    ...
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Is there a way to set the image dynamically to show different icons on the map? I have subclassed MKPointAnnotation to add a parameter telling me which kind of entry this is and depending on that I want to show a different icon, but I didn't find a way to make this solution dynamic as annotation and reuseIdentifier are nil when setting the image. – Philipp Rosengart Oct 13 '22 at 14:43
  • 1
    Yeah, you just move the setting of `glyphImage` into the `configure` method. Personally, I give my annotation (a `MKPointAnnotation` subclass) a custom property and check that. – Rob Oct 13 '22 at 17:12
  • Thanks @Rob. But I have some questions as well. I want this annotation to look like points of interest that apple serves by default, i.e - smaller (lets say 24 by 24) and the title adjacent to it. What is the best way to do this? Subclassing MKAnnotationView would open a huge can of worms so wondering what is the best way to handle this? – p0lAris Jun 05 '23 at 00:06
  • I’m not understanding the question: Why not just subclass `MKMarkerAnnotationView`, like shown above, and change the `glyphImage` accordingly? That lets you change the glyph and color and give you all the other behaviors for free. If you are looking for something more fundamentally different, then you’re looking at `MKAnnotationView` and re-implement the desired behaviors, and all the work that entails. – Rob Jun 05 '23 at 13:05