iOS Swift MapKit Custom Annotation

I recommend subclassing `MKPointAnnotation.

Pokémon Pin

I have included only the necessary code to display a custom map pin. Think of it as a template.

Outline

  • We will create a point annotation object and assigning a custom image name with the CustomPointAnnotation class.

  • We will subclass the MKPointAnnotation to set image and assign it on the delegate protocol method viewForAnnotation.

  • We will add an annotation view to the map after setting the coordinate of the point annotation with a title and a subtitle.

  • We will implement the viewForAnnotation method which is an MKMapViewDelegate protocol method which gets called for pins to display on the map. viewForAnnotation protocol method is the best place to customise the pin view and assign a custom image to it.

  • We will dequeue and return a reusable annotation for the given identifier and cast the annotation to our custom CustomPointAnnotation class in order to access the image name of the pin.

  • We will create a new image set in Assets.xcassets and place [email protected] and [email protected] accordingly.

  • Don't forget plist.

NSLocationAlwaysUsageDescription and NSLocationWhenInUseUsageDescription

enter image description here

As always test on a real device.

The swizzle 🌀

//1

CustomPointAnnotation.swift

import UIKit
import MapKit

class CustomPointAnnotation: MKPointAnnotation {
var pinCustomImageName:String!
}

//2

ViewController.swift

import UIKit
import MapKit

class ViewController: UIViewController, MKMapViewDelegate,  CLLocationManagerDelegate {


@IBOutlet weak var pokemonMap: MKMapView!
let locationManager = CLLocationManager()
var pointAnnotation:CustomPointAnnotation!
var pinAnnotationView:MKPinAnnotationView!

override func viewDidLoad() {
    super.viewDidLoad()

    //Mark: - Authorization
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestAlwaysAuthorization()
    locationManager.startUpdatingLocation()

    pokemonMap.delegate = self
    pokemonMap.mapType = MKMapType.Standard
    pokemonMap.showsUserLocation = true

}

func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    let location = CLLocationCoordinate2D(latitude: 35.689949, longitude: 139.697576)
    let center = location
    let region = MKCoordinateRegionMake(center, MKCoordinateSpan(latitudeDelta: 0.025, longitudeDelta: 0.025))
    pokemonMap.setRegion(region, animated: true)

    pointAnnotation = CustomPointAnnotation()
    pointAnnotation.pinCustomImageName = "Pokemon Pin"
    pointAnnotation.coordinate = location
    pointAnnotation.title = "POKéSTOP"
    pointAnnotation.subtitle = "Pick up some Poké Balls"

    pinAnnotationView = MKPinAnnotationView(annotation: pointAnnotation, reuseIdentifier: "pin")
    pokemonMap.addAnnotation(pinAnnotationView.annotation!)
}

func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
    print(error.localizedDescription)
}

//MARK: - Custom Annotation
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
    let reuseIdentifier = "pin"
    var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseIdentifier)

    if annotationView == nil {
        annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
        annotationView?.canShowCallout = true
    } else {
        annotationView?.annotation = annotation
    }

    let customPointAnnotation = annotation as! CustomPointAnnotation
    annotationView?.image = UIImage(named: customPointAnnotation.pinCustomImageName)

    return annotationView
}
}

There are a few issues you need to deal with.

MKMapView and annotations

Firstly, it is necessary to understand how MKMapView displays an annotation view from an annotation. There are

  1. MKMapView - displays the map and manages annotations.
  2. MKMapViewDelegate - you return data from specific functions to MKMapView.
  3. MKAnnotation - contains data about a location on the map.
  4. MKAnnotationView - displays an annotation.

An MKAnnotation holds the data for a location on the map. You create this data and hand it to MKMapView. At some point in the future, when the map view is ready to display the annotation it will call back to the delegate and ask it to create an MKAnnotationView for an MKAnnotation. The delegate creates and returns the view and the map view displays it. You specify the delegate in the storyboard, or in code e.g. mapView.delegate = self.

Location

Tracking the users location is complicated by:

  1. Permission is needed from the user before location tracking is enabled.
  2. There is a delay after the user allows tracking, until the user's location is available.
  3. Location services might not even be enabled.

Your code needs to deal with authorisation by checking CLLocationManager.authorizationStatus, and implementing CLLocationManagerDelegate methods.

Note that to use requestWhenInUseAuthorization requires entry for NSLocationWhenInUseUsageDescription in Info.plist

Example

Example project on GitHub.

import UIKit
import MapKit
import CoreLocation

class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {

    // Instance of location manager. 
    // This is is created in viewDidLoad() if location services are available.
    var locationManager: CLLocationManager?

    // Last location made available CoreLocation.
    var currentLocation: MKUserLocation? {
        didSet {
            // Hide the button if no location is available.
            button.hidden = (currentLocation == nil)
        }
    }

    // Date formatter for formatting dates in annotations.
    // We use a lazy instance so that it is only created when needed.
    lazy var formatter: NSDateFormatter = {

        let formatter = NSDateFormatter()
        formatter.timeStyle = NSDateFormatterStyle.ShortStyle
        formatter.dateStyle = NSDateFormatterStyle.ShortStyle
        return formatter
    }()

    @IBOutlet var button: UIButton!
    @IBOutlet var mapView: MKMapView!

    override func viewDidLoad() {

        super.viewDidLoad()

        mapView.delegate = self

        // Track the user's location if location services are enabled.
        if CLLocationManager.locationServicesEnabled() {

            locationManager = CLLocationManager()
            locationManager?.delegate = self
            locationManager?.desiredAccuracy = kCLLocationAccuracyBest

            switch CLLocationManager.authorizationStatus() {

            case .AuthorizedAlways, .AuthorizedWhenInUse:
                // Location services authorised.
                // Start tracking the user.
                locationManager?.startUpdatingLocation()
                mapView.showsUserLocation = true

            default:
                // Request access for location services.
                // This will call didChangeAuthorizationStatus on completion. 
                locationManager?.requestWhenInUseAuthorization()
            }
        }
    }

    //
    // CLLocationManagerDelegate method
    // Called by CLLocationManager when access to authorisation changes.
    //
    func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {

        switch status {

        case .AuthorizedAlways, .AuthorizedWhenInUse:
            // Location services are authorised, track the user.
            locationManager?.startUpdatingLocation()
            mapView.showsUserLocation = true

        case .Denied, .Restricted:
            // Location services not authorised, stop tracking the user.
            locationManager?.stopUpdatingLocation()
            mapView.showsUserLocation = false
            currentLocation = nil

        default:
            // Location services pending authorisation.
            // Alert requesting access is visible at this point.
            currentLocation = nil
        }
    }

    //
    // MKMapViewDelegate method
    // Called when MKMapView updates the user's location.
    //
    func mapView(mapView: MKMapView, didUpdateUserLocation userLocation: MKUserLocation) {

        currentLocation = userLocation
    }

    @IBAction func addButtonTapped(sender: AnyObject) {

        guard let coordinate = currentLocation?.coordinate else {
            return
        }

        let reportTime = NSDate()
        let formattedTime = formatter.stringFromDate(reportTime)

        let annotation = MKPointAnnotation()
        annotation.title = "Annotation Created"
        annotation.subtitle = formattedTime
        annotation.coordinate = coordinate

        mapView.addAnnotation(annotation)
    }

    //
    // From Bhoomi's answer. 
    //
    // MKMapViewDelegate method
    // Called when the map view needs to display the annotation.
    // E.g. If you drag the map so that the annotation goes offscreen, the annotation view will be recycled. When you drag the annotation back on screen this method will be called again to recreate the view for the annotation.
    //
    func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {

        guard !annotation.isKindOfClass(MKUserLocation) else {

            return nil
        }

        let annotationIdentifier = "AnnotationIdentifier"

        var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(annotationIdentifier)

        if annotationView == nil {
            annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
            annotationView!.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
            annotationView!.canShowCallout = true
        }
        else {
            annotationView!.annotation = annotation
        }

        annotationView!.image = UIImage(named: "smile")

        return annotationView

    }
}

check your image.png in your project bundle or Assets.xcassets

 func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {

    guard !annotation.isKind(of: MKUserLocation.self) else {
        return nil
    }

    let annotationIdentifier = "AnnotationIdentifier"

    var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationIdentifier)
    if annotationView == nil {
        annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
        annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        annotationView!.canShowCallout = true
    }
    else {
        annotationView!.annotation = annotation
    }

    annotationView!.image = UIImage(named: "image.png")

    return annotationView
}

Do as follow may be work for you.

1) Create custom class for the Annotation Pin.

class CustomPointAnnotation: MKPointAnnotation {
    var imageName: UIImage!
}

2)Define variable as below.

var locationManager = CLLocationManager()

3) Call below method in viewDidLoad()

  func checkLocationAuthorizationStatus() {
        if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
            map.showsUserLocation = false
        } else {
            locationManager.requestWhenInUseAuthorization()
        }
    }

4) Put below code in viewWillAppear()

    self.map.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestAlwaysAuthorization()
    locationManager.delegate = self
    dispatch_async(dispatch_get_main_queue(),{
        self.locationManager.startUpdatingLocation()
    })

5) Most important implement below method.

 func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
        if !(annotation is CustomPointAnnotation) {
            return nil
        }

        let reuseId = "Location"

        var anView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
        if anView == nil {
            anView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
            anView!.canShowCallout = true
        }
        else {
            anView!.annotation = annotation
        }
        let cpa = annotation as! CustomPointAnnotation
        anView!.image = cpa.imageName

        return anView
    } 

6) Execute below code where you you have received custom pin image

   let strLat = "YOUR LATITUDE"
   let strLon = "YOUR LONGITUDE"
   let info = CustomPointAnnotation()
   info.coordinate = CLLocationCoordinate2DMake(strLat.toDouble()!,strLon.toDouble()!)
   info.imageName = resizedImage
   info.title = dict!["locationName"]! as? String
   self.map.addAnnotation(info)