iOS Swift MapKit Custom Annotation [closed]
Asked Answered
I

5

17

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

Interfertile answered 8/7, 2016 at 19:31 Comment(11)
Variable names should start with lowercase lettersGuanine
@Guanine I assume you're referencing the MainMap variable?Interfertile
And all the other ones that don't start with lowercase lettersGuanine
@Guanine Do you think that will fix the issue?Interfertile
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.Guanine
@Guanine ...So you only read the first sentence of the question then?Interfertile
Your problem statement is unclear. You need to explicitly say exactly what your problem is and provide a Minimal, Complete, and Verifiable example of your issue. Don't just dump your code.Sportswear
@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: developer.apple.com/library/ios/documentation/UserExperience/…Bakken
@Sportswear and LukeVanIn new information has been added. Also I will take a look at the MapKit guide.Interfertile
@LukeVanIn new information addedInterfertile
why is it closed?Armyworm
S
59

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
}
}
Sparerib answered 17/7, 2016 at 8:55 Comment(6)
So where exactly do I put the name of the image? Like "image.png"Interfertile
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".Sparerib
the above code doesnt work in swift 3.. crashes at let customPointAnnotation = annotation as! CustomPointAnnotation ...Cohligan
Change it to if let customPointAnnotation = annotation as? CustomPointAnnotation to avoid crashLiber
didn't work not showing pin on map.Hostelry
don't you need to remove the old pointAnnotation?Armyworm
B
15

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

    }
}
Bakken answered 14/7, 2016 at 20:21 Comment(0)
E
7

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
}
Encapsulate answered 14/7, 2016 at 11:21 Comment(8)
Hi!.Does it work for you?Encapsulate
Do I have to call this function when the button is pressed on the storyboard?Interfertile
It's delegate method of mapview..It's a method for view of annotation(pin).If you want to add pin when button click then put addAnnotation code in your button click.Encapsulate
You can accept my answer if you can solve your problem.It's encourage to help more.Encapsulate
Hi..is this not work for you?If works you can do accept my answer.Encapsulate
My mac actually broke, i have a new iMac coming in within the next few days so I can try these solutions thenInterfertile
But your bunty point will be expire soon if you not accept any answer then you and other will not get any point and it will waste so better accept any answer if any useful.Encapsulate
@BhoomiJagani: It's not bunty points...Instead bounty points ;-) Sadly, this answer didn't worked.Athene
R
5

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)
Roberts answered 18/7, 2016 at 5:31 Comment(1)
let cpa = annotation as! CustomPointAnnotation is the magic line!Widmer
P
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 =)

Palaeo answered 13/7, 2016 at 5:50 Comment(3)
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.Interfertile
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 ResourcesPalaeo
Okay I will check that soon. Thanks, hopefully this works.Interfertile

© 2022 - 2024 — McMap. All rights reserved.