14

I am having some trouble getting a custom annotation to load inside of my map view when I try to place a pin.

import UIKit
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate{
@IBAction func ReportBtn(sender: AnyObject) {
    //MARK: Report Date And Time Details
    let ReportTime = NSDate()
    let TimeStamp = NSDateFormatter()
    TimeStamp.timeStyle = NSDateFormatterStyle.ShortStyle
    TimeStamp.dateStyle = NSDateFormatterStyle.ShortStyle
    TimeStamp.stringFromDate(ReportTime)
    //MARK: Default Point Annotation Begins
    let ReportAnnotation = MKPointAnnotation()
    ReportAnnotation.title = "Annotation Created"
    ReportAnnotation.subtitle = ReportTime.description
    ReportAnnotation.coordinate = locationManager.location!.coordinate
    mapView(MainMap, viewForAnnotation: ReportAnnotation)
    MainMap.addAnnotation(ReportAnnotation)
}

@IBOutlet weak var MainMap: MKMapView!
let locationManager = CLLocationManager()

override func viewDidLoad() {
    super.viewDidLoad()
    self.locationManager.requestWhenInUseAuthorization()
    self.locationManager.delegate = self
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
    self.locationManager.startUpdatingLocation()
    self.MainMap.showsUserLocation = true
}


//MARK: - Location Delegate Methods
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
 let location = locations.last
 let center = CLLocationCoordinate2D(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
 let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02 ))
    self.MainMap.setRegion(region, animated: true)
    //self.locationManager.stopUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError){
    print(error.localizedDescription)
}
//MARK:Custom Annotation Begins Here
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
    guard !annotation.isKindOfClass(MKUserLocation) else {
        return nil
    }
    /*if annotation.isKindOfClass(MKUserLocation){
        //emty return, guard wasn't cooperating
    }else{
    return nil
    }*/
    let annotationIdentifier = "AnnotationIdentifier"

    var annotationView: MKAnnotationView?
    if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(annotationIdentifier){
        annotationView = dequeuedAnnotationView
        annotationView?.annotation = annotation
    }
    else{
        let av = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
        av.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
        annotationView = av
    }
    if let annotationView = annotationView {
        annotationView.canShowCallout = true
        annotationView.image = UIImage(named: "image.png")
    }
    return annotationView

}
}

Added Information

I am positive that the button functionality works perfect. With the current code, dumped above, the default red pin annotation appears right where it should. When I tap on the pin, the description I specified also appears without an issue. The only problem I am having with this code is that I cannot get my image to take the place of the boring, default red pin

Cœur
  • 32,421
  • 21
  • 173
  • 232
Ethan
  • 1,705
  • 1
  • 16
  • 44
  • 4
    Variable names should start with lowercase letters – dan Jul 08 '16 at 19:49
  • @dan I assume you're referencing the `MainMap` variable? – Ethan Jul 08 '16 at 20:07
  • 3
    And all the other ones that don't start with lowercase letters – dan Jul 08 '16 at 20:15
  • @dan Do you think that will fix the issue? – Ethan Jul 09 '16 at 00:04
  • 1
    It won't do anything except make your code easier to read. I didn't even know you had an issue to be honest, you never mention one in your question. Sounded like you wanted a code review. – dan Jul 09 '16 at 00:58
  • 1
    @dan ...So you only read the first sentence of the question then? – Ethan Jul 10 '16 at 06:16
  • 1
    Your problem statement is unclear. You need to explicitly say exactly what your problem is and provide [a Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) of your issue. Don't just dump your code. – JAL Jul 12 '16 at 13:06
  • @user154248 What else can you add to help describe the problem. For example - do you know for sure that `ReportBtn` is being called. How about `viewForAnnotation`. If you're unsure, try adding a breakpoint, or a print statement. SO generally responds better to questions when you can show that you have tried to narrow down the problem. Also, have a look at the Annotations section of the MapKit Programming Guide: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/LocationAwarenessPG/AnnotatingMaps/AnnotatingMaps.html#//apple_ref/doc/uid/TP40009497-CH6-SW1 – Luke Van In Jul 12 '16 at 15:59
  • @JAL and LukeVanIn new information has been added. Also I will take a look at the MapKit guide. – Ethan Jul 13 '16 at 04:45
  • @LukeVanIn new information added – Ethan Jul 14 '16 at 18:27
  • 1
    why is it closed? – Vyachaslav Gerchicov Nov 29 '19 at 13:26

5 Answers5

57

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 image@3x.png and image@2x.png 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
}
}
Edison
  • 10,891
  • 4
  • 36
  • 46
  • 1
    So where exactly do I put the name of the image? Like "image.png" – Ethan Jul 21 '16 at 19:19
  • 3
    The name of the pngs doesn't matter because "Pokemon Pin" is the name of the Image Set inside of the blue Assets.xcassets folder. When you select the "Pokemon Pin" Image Set you will see the usual placeholders for x1 x2 x3 images. Those pngs can be named anything. It's the name of the Image Set that is important because it has to match `pointAnnotation.pinCustomImageName = "Pokemon Pin"`. – Edison Jul 22 '16 at 00:42
  • 5
    the above code doesnt work in swift 3.. crashes at let customPointAnnotation = annotation as! CustomPointAnnotation ... – the_legend_27 May 04 '17 at 03:14
  • 1
    Change it to `if let customPointAnnotation = annotation as? CustomPointAnnotation ` to avoid crash – skornos Nov 15 '17 at 13:23
  • didn't work not showing pin on map. – Arshad Shaik Jul 19 '19 at 07:30
  • don't you need to remove the old `pointAnnotation`? – Vyachaslav Gerchicov Nov 29 '19 at 13:11
14

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

    }
}
Luke Van In
  • 5,010
  • 2
  • 22
  • 45
6

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
}
Jayprakash Dubey
  • 32,447
  • 16
  • 161
  • 169
Bhoomi Jagani
  • 2,353
  • 15
  • 23
4

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)
Hitesh Surani
  • 9,976
  • 5
  • 37
  • 55
2

From the code and according to the MapKit guide, your code look correct. I am thinking that it could be this line annotationView.image = UIImage(named: "image.png")

Is there a chance that image.png could be the wrong image name or not added in to the project when compile? Also just fyi, if you are using .xcassets, you does not have to add a .png.

As annotationView.image is a optional, when the image UIImage(named: "image.png") is nil, it will not crash but just render the default pin image.

If this is not the issue, please provide more info on the debugging steps that you have taken so the rest of us can understand better and help you. Cheers =)

Zac Kwan
  • 4,597
  • 3
  • 16
  • 24
  • All I did to bring the picture into the project was copy and paste it in. Am I supposed to do something else to get it to load in? I know the picture name is equal to the reference name in the code though. – Ethan Jul 13 '16 at 06:09
  • It is recommended to use the `Assets.xcassets`. You could just drag image into the `Assets.xcassets` and also manage all your future assets from there. If you really does not want to, at least check that the image is being added into the `Copy Bundle Resources` under your project `Target > Build Phrase > Copy Bundle Resources` – Zac Kwan Jul 13 '16 at 06:20
  • Okay I will check that soon. Thanks, hopefully this works. – Ethan Jul 13 '16 at 06:39