Building the perfect iOS Map (II): Completely custom annotation views

One of the most recurrent topics when working with the MapKit framework and annotations is customising the annotations and callout views in the map to match the design of the application we are working on. Unfortunately, unless you want to stick to the most basic, iOS-looking styling, it’s not easy at all to do so, and the MapKit framework does not really give you the flexibility that you need for customising your map.

Apple example source code “MapCallouts: Using MapKit Annotations” is just hideous, and doesn’t really address the problem of customising the map annotation views. There, in the image, you might see several pins. One of them (the one at the bottom with a “Tea garden” title and an image) might seem like a custom callout, but it’s not, it’s the annotation view. You cannot click on it (at least, nothing happens if you do so), it cannot contain buttons or any interactive elements… it’s just not what you would probably need for a modern application, and the other two pins will show you a “somewhat” customised callout, but you won’t be able to get a fully customised callout view using this code sample.

There are some code samples out there talking about customisation, but they either don’t get a full customisation or you end up with a callout whose elements are not interactive, i.e: buttons cannot be pushed, table views cannot be scrolled, etc…

In this post, I describe a method for making completely custom annotation views that actually work, and are fully interactive (including inner buttons, table views, etc). As always, you can download the full sample code project in my Github repository.

The problem with annotations and callouts in MapKit

This figure shows the structure of a map with all the elements. The little red pin you are probably used to see in Maps applications is the annotation view, and the bubble that shows up when you click on it its called the callout view, and it’s the default callout that MapKit offers for you to show. Now, if asked “what parts of the callout view can you completely customise?” you would be tempted to say “all of them”, but actually, MapKit only gives you control on the central one, the “Detail Callout Accessory View”. The left and right callout button views, which are commonly used to show buttons and images, can be hidden, so that’s not a problem. The really annoying element here is the title UILabel in orange.

If you really want to customise your annotation callouts, this label is a real annoyance. You can edit the text and some properties, but MapKit won’t give you full access to this title label. If you set the label to be empty, the label won’t show… because the whole bubble will not show at all! And if you set it to a single space (” “), you will get the bubble and no title, but of course the space for the label will be there (nice job, Apple), so the only way to remove it is through a hacky method involving iterating through the detail callout accessory view superviews hierarchy, finding the title label and removing it. This solution is not only hacky/patchy and vulnerable to MapKit changes, but it has some nasty side problems like the autolayout animation expanding the bubble to its full width and then removing the label and shrinking the bubble in a clearly visible and unprofessional effect.

Thus, the only real way of customising the callouts is setting the annotation view’s property “canShowCallout” to false and taking care of the whole process of showing the callout, dismissing it, making sure that the interactive elements work, etc. This last part is specially difficult, and it’s usually left apart when discussing on custom annotations and callouts. So all posts and articles I’ve found out there lack addressing the interactivity of the elements. We’ll take care to make all controls fully actionable.

Our sample application

Our sample application is called “Your Personal Wishlist”, it has a list of people around the world, and each person has a wishlist of things they would like to buy.

These people are represented in the map as pins (we will use a custom blue flag pin), and when one pin is clicked, the card for this person will show up in the callout, completely customised.

The bubble view will be contained in a Nib/Xib file, so we will have full control on its design.

The list of elements is actually a UITableView, and we will be able to scroll through it. The button is also clickable, and when pressed, will take us to another screen where we can see a detailed view of this person and their wishlist.

Hands on!

First we will add a MapView to our view controller, add MapKit and pin the map to the edges of the screen. We will also set the delegate of the map to be our view controller.

Next, we will create our model. We will need a Person class to define a person, with a name, avatar, coordinate and wishlist.

class Person: NSObject {
   var name: String
   var avatar: UIImage
   var wishList = [String]()
   var location: CLLocationCoordinate2D = kCLLocationCoordinate2DInvalid
   ...
}

Then, we will need a singleton class to manage the creation of the people from around the globe and initialise the model that we will show in our map:

// quick singleton implementation for our PeopleWishListManager.
private let _singletonInstance = PeopleWishListManager()

private let kPeopleWishListManagerNumberOfPeople = 21

class PeopleWishListManager: NSObject {
  // shared instance of PeopleWishListManager.
  class var sharedInstance: PeopleWishListManager { return _singletonInstance }
 
  // people wishlist
  var people = [Person]()
 
  // MARK: - init
  override init() {
    super.init()
    populatePeopleList()
  }

  func populatePeopleList() {
    let names = ["Oren Nimmons", "Flor Addington", "Bernadette Bachus", ...]
 
    let coordinates = [CLLocationCoordinate2D(latitude: 47.57273, longitude: -52.68997)...]
 
    people = []
    for i in 0..<kPeopleWishListManagerNumberOfPeople {
      let wishlist = giveMeAWishList()
      let name = names[i]
      let avatar = UIImage(named: "avatar\(i+1)")!
 
      let person = Person(name: name, avatar: avatar)
      person.wishList = wishlist
      person.location = coordinates[i]
      people.append(person)
   }
 }
 
 func giveMeAWishList() -> [String] {
   let items = ["Watch", "Purple pen",..., "bottle of perfume"]
 
   let num = arc4random_uniform(3) + 1
   var wishlist = [String]()
   for _ in 0..<num {
      let index = Int(arc4random_uniform(UInt32(items.count)))
      wishlist.append(items[index])
   }
   return wishlist
  }
}

I just picked a series of names, coordinates, and a list of random objects. I also used some free icons I found from the guys at “Creative Tail”. With all this information in place, we are in the position of populating a list of 21 people.

Now that we have our main view controller and model in place, it’s time to take care of the elements of our map, the annotations and callouts.

Building our annotation and annotation view

The first step is building our annotation and annotation view. The annotation is the “model” part of the equation, and carries the coordinate and main any piece of information needed to be displayed in the annotation view. The annotation view is the pin you see on the map, and controls the interactions that happen when you click on it. Given that we are completely customising the annotation and callout, the annotation view will have to show the callout when the annotation is clicked, dismissing any previous annotation. We will take care of that thanks to the selected/unselected state of the annotation (via the “setSelected” method), to show the callout when selected and dismiss it when unselected.

Our annotation will be a NSObject implementing theMKAnnotation protocol, and will simply contain the person and a coordinate:

class PersonWishListAnnotation: NSObject, MKAnnotation {
  var person: Person
  var coordinate: CLLocationCoordinate2D { return person.location }
 
  init(person: Person) {
     self.person = person
     super.init()
  }
 
  var title: String? {
    return person.name
  }
 
  var subtitle: String? {
    return person.wishList.joined(separator: ", ")
  }
}

Notice how we defined title and subtitle, even though we are not going to need them (as we won’t be using MapKit default callout accessory view).

Now it’s the time to define our annotation view. It will be a subview of the MKAnnotationView class.

class PersonWishListAnnotationView: MKAnnotationView {
  // data
  weak var customCalloutView: PersonDetailMapView?
  override var annotation: MKAnnotation? {
     willSet { customCalloutView?.removeFromSuperview() }
  }
 
  // MARK: - life cycle
 
  override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
    super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    self.canShowCallout = false // 1
    self.image = kPersonMapPinImage
  }
 
  required init?(coder aDecoder: NSCoder) {
     super.init(coder: aDecoder)
    self.canShowCallout = false // 1
    self.image = kPersonMapPinImage
  }
 
  // MARK: - callout showing and hiding
  override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
 
    if selected { // 2
      self.customCalloutView?.removeFromSuperview() // remove old custom callout (if any)
 
      if let newCustomCalloutView = loadPersonDetailMapView() {
        // fix location from top-left to its right place.
        newCustomCalloutView.frame.origin.x -= newCustomCalloutView.frame.width / 2.0 - (self.frame.width / 2.0)
        newCustomCalloutView.frame.origin.y -= newCustomCalloutView.frame.height
 
        // set custom callout view
        self.addSubview(newCustomCalloutView)
        self.customCalloutView = newCustomCalloutView
 
        // animate presentation
        if animated {
          self.customCalloutView!.alpha = 0.0
          UIView.animate(withDuration: kPersonMapAnimationTime, animations: { 
            self.customCalloutView!.alpha = 1.0
          })
        }
      }
    } else { // 3
      if customCalloutView != nil {
        if animated { // fade out animation, then remove it.
          UIView.animate(withDuration: kPersonMapAnimationTime, animations: { 
            self.customCalloutView!.alpha = 0.0
          }, completion: { (success) in
            self.customCalloutView!.removeFromSuperview()
          })
        } else { self.customCalloutView!.removeFromSuperview() } // just remove it.
      }
    }
  }
 
  func loadPersonDetailMapView() -> UIView? { // 4
    let view = UIView(frame: CGRect(x: 0, y: 0, width: 240, height: 280))
    return view
  }

  override func prepareForReuse() { // 5
    super.prepareForReuse()
    self.customCalloutView?.removeFromSuperview()
  }
}

Let’s go through this code step by step:

  1. First, we need to make sure that we set canShowCallout to false, to avoid showing the default MapKit callout bubble. Then we define the image that we are going to use for our pin.
  2. the setSelected method is the right place to show and dismiss our custom callout. When called, we will need to differentiate if the annotation view has been selected or deselected. The first case implies selecting the annotation. In this case we should show the callout, but we must be careful to dismiss any previous annotation callout being shown using removeFromSuperview() (this happens if you select a pin and then select another one, we must dismiss the first callout and show the second one). We will store our custom callout in an instance variable called customCalloutView, and use the method “loadPersonDetailMapView” to load it. At the moment, as we haven’t designed our custom view yet, we will just return a rectangular white UIView. After adding this view as a subview of the annotation, we will need to adjust it’s position. When first added, the view will be shown from the top-left corner of the pin, covering it, while we want to show it horizontally centered and with the bottom side of the callout view at the top of the pin (displaying as a bubble on top center of the pin), so we need to adjust the X and Y positions as shown.
  3. In case of deselecting the annotation, we just dismiss the custom callout view (if any).
  4. This is the method where we would load our custom callout view. For the moment we will just return a 240×280 white rectangle.
  5. when prepareForReuse is called, the annotation is going to be reused from the current pin position to a new one, so we need to remove the custom callout.

Now we can adjust our ViewController to show our annotations in the map:

class ViewController: UIViewController, MKMapViewDelegate, PersonDetailMapViewDelegate {
  // outlets
  @IBOutlet weak var mapView: MKMapView!
 
  // data
  var selectedPerson: Person?
 
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    configurePeopleInMap()
  }
 
  func configurePeopleInMap() {
    var annotations = [MKAnnotation]()
    for person in PeopleWishListManager.sharedInstance.people {
      let annotation = PersonWishListAnnotation(person: person)
      annotations.append(annotation)
    }
    mapView.removeAnnotations(mapView.annotations)
    mapView.addAnnotations(annotations)
  }
 
  // MARK: - MKMapViewDelegate methods
  func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
    let visibleRegion = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 10000, 10000)
    self.mapView.setRegion(self.mapView.regionThatFits(visibleRegion), animated: true)
  }

  func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    if annotation is MKUserLocation { return nil }
    var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: kPersonWishListAnnotationName)
 
    if annotationView == nil {
      annotationView = PersonWishListAnnotationView(annotation: annotation, reuseIdentifier: kPersonWishListAnnotationName)
      (annotationView as! PersonWishListAnnotationView).personDetailDelegate = self
    } else {
      annotationView!.annotation = annotation
    }
    return annotationView
  }

}

In viewWillAppear we call configurePeopleInMap(), that will just call our PeopleWishListManager and initialise the annotations with the Person instances. then in mapView:viewForAnnotation: we will dequeue the appropriate PersonWishListAnnotationView that corresponds to that annotation.

If you run this code right now, you will notice that the pins are correctly shown, but when we click on any of them, we just get an empty view. That’s alright, let’s design our custom callout view next!

Designing our Custom Callout View

Our custom callout view will contain a fancy design of the person’s profile, including the wishlist and a “See details” button to segue to another view controller showing the details for that person.

We will need to create the UIView and the Nib/Xib file independently. First, we will create a new file in “User Interface” -> “View”. This will be our Nib view file, and we will call it “PersonDetailMapView.xib”. We will add all the elements and the autolayout constraints. Next, we will add a new file in “Source” -> “Cocoa Touch Class” and make it a “UIView” type of object. We will call this file “PersonDetailMapView.swift”.

Then we need to link them together. In order to do that, we will go to the hierarchy, select the root view, and go to the inspector, to the “Custom Class” tab and enter “PersonDetailMapView”. It’s important not to specify that in the File Owner’s or any other place but in the main view. It’s a shame Apple still doesn’t include a UIView NIB/Swift combo like the ones for UIViewController or UITableViewCell.

Now we can add our outlets for all the elements in the IB view. Once done, it’s important to go to the swift code and set our view as the delegate and data source for the UITableView, and also define the IBAction for the “See Details” button.

 

protocol PersonDetailMapViewDelegate: class { // 1 
  func detailsRequestedForPerson(person: Person)
}

class PersonDetailMapView: UIView, UITableViewDelegate, UITableViewDataSource {
  // outlets
  @IBOutlet weak var backgroundContentButton: UIButton!
  @IBOutlet weak var personImageView: UIImageView!
  @IBOutlet weak var personName: UILabel!
  @IBOutlet weak var wishListTableView: UITableView!
  @IBOutlet weak var seeDetailsButton: UIButton!
 
  // data
  var person: Person!
  weak var delegate: PersonDetailMapViewDelegate?
 
  override func awakeFromNib() {
    super.awakeFromNib()
 
    // setup list
    wishListTableView.register(UINib(nibName: "PersonWishListItemTableViewCell", bundle: nil), forCellReuseIdentifier: "PersonWishListItemTableViewCell")
    wishListTableView.delegate = self   // 2
    wishListTableView.dataSource = self // 2
 
    // appearance
    backgroundContentButton.applyArrowDialogAppearanceWithOrientation(arrowOrientation: .down) // 3
  }
 
  @IBAction func seeDetails(_ sender: Any) { // 4
    delegate?.detailsRequestedForPerson(person: person)
  }
 
  func configureWithPerson(person: Person) { // 5
    self.person = person
 
    personImageView.image = person.avatar
    personName.text = person.name
    wishListTableView.reloadData()
  }
 
  // MARK: - UITableViewDelegate/DataSource methods
 
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return person?.wishList.count ?? 0
  }
 
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 6
    let cell = tableView.dequeueReusableCell(withIdentifier: "PersonWishListItemTableViewCell", for: indexPath) as! PersonWishListItemTableViewCell
    if let item = person?.wishList[indexPath.row] { cell.configureWithItem(item: item) }
    return cell
  }
 
}

Let’s analyse this code step by step:

  1. We will set a protocol to have our delegate know when the user clicks on the “See Details” button.
  2. it’s important to set ourselves as the datasource/delegate of the table view in order for it to be populated and react to user interaction.
  3. When we refuse to use the default MapKit callout, we need to take care of everything, and that includes giving the proper bubble appearance to the callout and setting the arrow at the bottom. This category of UIView, with the applyArrowDialogAppearanceWithOrientation() method, just creates a Bezier path around the view with a bottom arrow. If you want to know more about Bezier paths and applying custom shapes to views and graphic elements, please have a look at my post “Bezier paths in practice (I): From basic shapes to custom designable controls“.
  4. This IBAction calls our delegate when the button is pressed.
  5. The configureWithPerson(person:) method will configure the cell for certain person, set the outlets, images, names, and reload the wishlist.
  6. The table view will contain simple rows, composed of just a label with the wishlist item’s name. Notice how we loaded the Nib for this custom cell in the viewDidLoad method. This Nib is really simple, has just one outlet, and looks like this:

Now that we have our custom callout view properly designed, and linked to the right behaviour, we can modify our PersonWishListAnnotationView to return an instance of this custom view in the loadPersonDetailMapView method. We will redefine it as follows, returning now a PersonDetailMapView instead of a simple UIView:

func loadPersonDetailMapView() -> PersonDetailMapView? {
  if let views = Bundle.main.loadNibNamed("PersonDetailMapView", owner: self, options: nil) as? [PersonDetailMapView], views.count > 0 {
    let personDetailMapView = views.first!
    personDetailMapView.delegate = self.personDetailDelegate
    if let personAnnotation = annotation as? PersonWishListAnnotation {
      let person = personAnnotation.person
      personDetailMapView.configureWithPerson(person: person)
    }
    return personDetailMapView
  }
  return nil
}

Here, we just load the view from the nib, and configure it with the proper person of this annotation view’s annotation.

If you run the application now, upon clicking on a pin, you will get the view, properly positioned agains the annotation in the map, and showing the right info for the selected person, hooray! But you will notice that we cannot click in the button or scroll through the list. All interaction seems to be captured by the map and not passing through the custom callout. This is exactly what’s happening, and in order to solve that, we need to specify some hit tests for the views we want to be interactive.

Adding Interactivity to our Custom Callout

The problem with our custom callout controls is that the map, and its associated gesture recognisers, are capturing all touches and interactions inside the map, so user touches don’t pass through to the button or the list. In order to solve that, we need to make use of the “hitTest” methods for both our annotation view and our custom callout view.

hitTest is a method that’s used to identify whether an interaction event (like a touch on a view) affected certain views or subviews of the control being tested. This method has multiple applications, but it’s relatively obscure and not well known to many programmers.

So our strategy will be the following: first, we will define hitTest in our PersonWishListAnnotationView custom annotation view. When a touch triggers this method, if the touch affected the superview or another external view, unrelated to the annotation or the custom callout, we’ll let it pass through to avoid affecting the normal behaviour of the map. However, if the touch event happened in our custom callout (supposing the annotation has been selected and the custom callout is showing), we will call the hitTest method on our custom callout view:

 
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
  // if super passed hit test, return the result
  if let parentHitView = super.hitTest(point, with: event) { return parentHitView }
  else { // test in our custom callout.
    if customCalloutView != nil {
      return customCalloutView!.hitTest(convert(point, to: customCalloutView!), with: event)
    } else { return nil }
  }
}

So, in our PersonDetailMapView custom callout view, we will also redefine the hitTest method, and we will check the hit test of every subview we might be interested in. This, in our current case, includes the “See Details” button and the UITableView list:

// MARK: - Hit test. We need to override this to detect hits in our custom callout.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
  // Check if it hit our annotation detail view components.

  // details button
  if let result = seeDetailsButton.hitTest(convert(point, to: seeDetailsButton), with: event) {
    return result
  }
  // list
  if let result = wishListTableView.hitTest(convert(point, to: wishListTableView), with: event) {
    return result
  }
  // fallback to our background content view
  return backgroundContentButton.hitTest(convert(point, to: backgroundContentButton), with: event)
}

As you might see, we included the background view covering the entirety of our custom callout view. This background view serves as a “cover” to the callout. If we add it, pinned to the edges, and return its hitTest by default when everybody else fails, we make sure that the callout is “touched” when no concrete control is (buttons, lists, etc), and so the callout won’t be dismissed (unselected) by the map when clicking inside.

Final touches

Now the rest is easy as pie. We just need to set our delegates in place from the ViewController to the PersonDetailMapView via the PersonWishListAnnotationView (ViewController -> PersonWishListAnnotationView -> PersonDetailMapView), add a new UIViewController for the details screen and make sure to do the segues and set the right person in prepareForSegue.

As always, you can download the full code sample project in my Github repository. If you have any question, please don’t hesitate to comment. Also, if you want to be always up to date about new posts in this series, join the “Digital Tips” list.

Where to go from here

In the previous post of this series, “Building The Perfect iOS Map (I): Synchronising a Map and a List“, we learned to synchronise a list (UITableView) with the map (MKMapView) to make them work together like in AirBnB or Idealista. In a future post, we will talk about faking the device location for debugging purposes, and about overlays. Stay tuned!

 

Comments(41)

jamie
March 24, 2017 At 2:51 pm

this tutorial is awesome!! thanks so much!

    Ignacio Nieto Carvajal
    March 24, 2017 At 2:51 pm

    Thanks Jamie! My pleasure to know that it was helpful 🙂

Paulo
March 31, 2017 At 2:51 pm

how are you ignacio? Nice turorial. I was just wondering of instead creating random users and uploading them to the map, how can I upload registered users from a certain database like firebase, and integrate them into the map. thanks

    Ignacio Nieto Carvajal
    April 5, 2017 At 2:51 pm

    Hi there Paulo!
    Sorry for my late reply. Well, that would be a completely different tutorial on its own :). You need to download the data from your users in firebase somehow and create the user entities as you download them. This, of course, is the summary, and as I told you is stuff worth a tutorial on its own.

Marlies
May 8, 2017 At 2:51 pm

Hi Ignacio!
Thanks for the great tutorial!
I was trying to recreate it with my own .xib. Everything works perfectly, except the callout looks like this https://snag.gy/jD5JLq.jpg (it’s only a white rectangle without the triangle at the bottom). I can’t find anything that’s different from your code and xib, other than mine uses StackViews and I don’t have a Person Object. The applyArrowDialogAppearanceWithOrientation(arrowOrientation: .down) function doesn’t do anything in my implementation. Do you have an idea were I went wrong?

    Ignacio Nieto Carvajal
    May 8, 2017 At 2:51 pm

    Hi there Marlies, thanks.
    It’s hard to know without having a look at the .xib file or the source code. Some things off the top of my head:
    1. Is the “background view” where you are calling applyArrowDialogAppearanceWithOrientation the only white view covering the callout? In my case, the button/view is the only element providing a white background, so when I cut it via the applyArrow… method, there’s nothing else “covering” the background with white.
    2. Are you leaving enough distance from the last element at the bottom of the callout to the bottom layout guide? In my case, I leave a distance (via a vertical distance constraint) between my button and the bottom edge of 43 pixels. This distance needs to be there.

    Hope it helps!

      Marlies
      May 29, 2017 At 2:51 pm

      Hi Ignacio,

      setting the opacity of the background of the view to 0% solved it for me, thank you so much!

        Ignacio Nieto Carvajal
        May 29, 2017 At 2:51 pm

        Nice to know Marlies! Thanks for sharing that. :)

Trevor Lyons
May 17, 2017 At 2:51 pm

Hi Ignacio, at the beginning of your tutorial you showed an image with what appears to be a custom annotation view from a nib/ xib. I currently have a project where I am loading my annotation view from an image but would like to change it to something that can contain more data without using a callout.

Do you have any quick advice for implementing this? I’m currently trying to load the .xib through the viewfor annotation function.

Thanks, Trevor.

    Ignacio Nieto Carvajal
    May 18, 2017 At 2:51 pm

    Hi there David,
    What’s shown at the beginning of the tutorial is Apple’s sample code for “custom annotations”. These are just the images that replace the pins, so it’s just an image. If you want to show some extra data, and this data needs to be somewhat interactive, you need a callout.
    However, if you can “draw” this data into an image (like text, for instance), then you can try doing it by putting an image instead of a pin, but I definitely don’t recommend that.

    Hope it helps!

Horatiu Flaviu Muresan
May 27, 2017 At 2:51 pm

Hello Ignacio! I have been looking all over the internet for a great tutorial for a custom MKMapView. Yours is the best so far. However, when I try to implement it in my own applications, the annotations won’t show on the map. I am designing an application for money exchange agencies to upload their services in order to be more discoverable and to get customers. I am wondering what I missed from the tutorial, since I am still debugging it for 4 days now. Maybe you can help, I will post the classes. I am getting the data from firebase, and add it to my custom AnnotationView.

Code: https://gist.github.com/anonymous/555af9686aa50fbb3434600fbfdd2d79

.xib files printscreen: https://prnt.sc/fcn4tc https://prnt.sc/fcn4pw

Console Output: https://gist.github.com/anonymous/7ac61a64540b9d8a131d2edd3c1a0540

Any help will be much appreciated. Thank you!

    Horatiu Flaviu Muresan
    May 27, 2017 At 2:51 pm

    Actually, I have found the issue.. The button name in my custom annotation view was mistyped. However, this tutorial works just fine, best I found on the web so far. Maybe the code I posted would help somebody else integrate this with firebase!

      Ignacio Nieto Carvajal
      May 27, 2017 At 2:51 pm

      Hi there! Sorry for not answering before. I’m glad you found the issue.

      Thanks for your kind words about my post, I’m happy to know you found it useful.

      Best!

Logan Koshenka
June 3, 2017 At 2:51 pm

Very helpful tutorial. I’m building an app I’ve been working on for a while and would love to speak more with you about custom MKAnnotation and MKAnnotationView classes. Do you do any sort of office hours or consulting? Please let me know. Regardless, super helpful tutorial and looking forward to more!

Matthias
September 25, 2017 At 2:51 pm

Hi Ignacio,

First of all – thank you very much for that awesome and detailed tutorial!

I have tried to integrate the custom annotations into my project, but I am stuck now…

Maybe you can help me from the far:)

I managed to integrate the model until the Point where you have the Array of Person objects to view in the map.

However, when the map opens I only get the default annotations. No custom map Pin and no custom annotation..

I am loading the custom annotations when a certain Button is clicked, Not when the view loads.

Can this be a problem?

If you need to see the project I can upload it somewhere for you.

Thanks,
Matthias

    Ignacio Nieto Carvajal
    September 27, 2017 At 2:51 pm

    Hi there Matthias, thanks for your comment, glad you liked it.
    It’s hard to tell without having a look at your code, but take into account that annotations cannot be added and then immediately get displayed because of how MapView and its delegate work. The annotations added to the map are displayed when the delegate viewForAnnotation: etc are called. Thus, it may happen that your annotations are being added after calling the delegate methods. If so, just refresh the map. Another possibility is that the class is not the delegate for the map view? That actually happens to me from time to time. I forget to add the delegate connection.

    Hope it helps!

      Matthias
      September 27, 2017 At 2:51 pm

      You are the man :)
      Thanks again for the tip with the delegate… I wasn’t searching there at all… kept debugging for hours and was wondering where the magic was in your example haha – obviously, one click+drag only

        Ignacio Nieto Carvajal
        September 27, 2017 At 2:51 pm

        No problem! Really glad to know it helped!
        It has happened to me sometimes. I’m banging my head against a wall for hours, and then someone suggests “have you checked this…?” and that’s exactly it :)

Judy Tsai
December 3, 2017 At 2:51 pm

Hi, Ignacio,

This is such a great tutorial. I am fairly new to swift and your tutorial is very easy to follow and to implement. However, I am running an issue when trying to load the locations (aka users) from the database.

The problem seems that the configurePeopleInMap() is called before the data was retrieved from the REST API JSON and therefore, the map shows no annotation.

I notice this because when I print within the configurePeopleInMap function, it returns the PeopleWishListManager.sharedInstance.people as empty array. But when I print the People variable within the function within the PeopleWishListManager.swift, it does return the array with values properly.

Is there anyway to make sure the array gets returned within the configurePeopleInMap function after it receives the value? I’ve been working on this since yesterday and still have yet found a solution…

Thank you so much for your help!!

    Ignacio Nieto Carvajal
    December 4, 2017 At 2:51 pm

    Hi there Judy. Thanks, glad to help!

    Yes, the main drawback of the MKMapView is that the annotations have to be ready when the map is displayed. What you can do is, once you receive the annotations from the RESTful call, add them to the map, calling the function again when you are sure the annotations are there.

    If you do, don’t forget to remove all previous annotations first (to avoid duplicating them), calling mapView.removeAnnotations(mapView.allAnnotations). Hope I understood your use case and was able to help!

      Judy
      December 5, 2017 At 2:51 pm

      Hi, Ignacio!

      Thank you so much for you help and quick reply! I was able to achieve what I needed with your feedback! I had to basically merge the list manager with the view controller to make sure the annotations are there before I call the function. I am sure this can be done as separate file as is but I am still new to swift and that worked for me. :)

        Ignacio Nieto Carvajal
        December 6, 2017 At 2:51 pm

        My pleasure, glad to know I was helpful :)

NasH
February 4, 2018 At 2:51 pm

Hi Ignacio,

Good job !

Just one thing about the hitTest management.

In fact, if you have another annotation (pin or flag) just below the current opened callout view, the hitTest will not go down in the custom callout hitTest

For now, I am not able to handle this particular case… :-/

    Ignacio Nieto Carvajal
    February 8, 2018 At 2:51 pm

    Hello there!

    Thanks, glad you liked it.

    It shouldn’t be any problem, just set user interaction to false on that other annotation and override hitTest on that class to return false. Let me know if it works!

    Best regards.

Ben
February 15, 2018 At 2:51 pm

Love the tutorial, It’s clear and concise only thing bugging me is the scrolling on your website. I can’t pin point what it is but its so awkward to use. Perhaps you could remove / fix whatever is causing this issue!

    Ignacio Nieto Carvajal
    February 16, 2018 At 2:51 pm

    Hello there Ben!

    Thanks for your words. Curiously, nobody has ever complained about any scrolling issue. I haven’t noticed anything myself. Can you elaborate on that? What device are you in?

      Ben
      February 18, 2018 At 2:51 pm

      Im using chrome on iMac however upon testing it in safari I can’t seem to replicate the issue.
      The best way I can describe it is there’s a sort of grid that the pages tries to fix to when scrolling, accelerating to that point and then quickly stopping.

        Ignacio Nieto Carvajal
        February 22, 2018 At 2:51 pm

        Uhm, I will make sure to test and fix that. Thanks for the info Ben! 🙏🏻

Darp
March 16, 2018 At 2:51 pm

Hi, Ignacio. Great Tutorial. This helped me a lot in my project.
The best tutorial on MKMapView, I have read.
thanks.

    Ignacio Nieto Carvajal
    March 16, 2018 At 2:51 pm

    Thanks Darp, a pleasure to know it was helpful! :)

Yury
March 18, 2018 At 2:51 pm

Hi Ignacio,

Thank you for the tutorial. Very handy.
Just one thing. I’m trying to figure out what is the backgroundContentButton. I can’t find it in your xib picture.

Thanks,
Yury

    Ignacio Nieto Carvajal
    March 22, 2018 At 2:51 pm

    Thanks Yury! The backgroundContentButton is the one “wrapping” the background so it closes the popup when you click outside. Hope that does make sense :)

Edward
March 29, 2018 At 2:51 pm

Thanks for the tutorial!

One thing that’s bugging me though is that some hits on the callout do actually pass to the map and thus moves the map or selects another pin..

In hittest in the Annotation View, in the debugger I notice that customCalloutView sometimes is equal to nil, despite it having been set earlier on for that annotation..

Any ideas?

Thanks in advance!

Ben Bejster
May 9, 2018 At 2:51 pm

Where is the implementation of personDetailDelegate ?

I can’t seem to find it.

Thanks!

    Ignacio Nieto Carvajal
    May 13, 2018 At 2:51 pm

    Hi there Ben, it’s on PersonDetailMapView.swift.
    Thanks!

joe
May 29, 2018 At 2:51 pm

Hi, thanks for the tutorial
I have just only one question if u can help me.
How can i place custom callout annotation view at the bottom of the screen and adjust its width according to the size of the screen ?

    Ignacio Nieto Carvajal
    May 30, 2018 At 2:51 pm

    Hello there Joe, thanks for your comment!

    Yes, you just need to edit the .xib file with the visual design and do the required modifications to the visual constraints. Hope this helps!

Giulio M
June 15, 2018 At 2:51 pm

This was pretty useful. Thanks!

    Ignacio Nieto Carvajal
    June 17, 2018 At 2:51 pm

    Thanks Giulio! Our pleasure to know you liked it!

MapKit(swift4) | iPhoneDeveloper
July 11, 2018 At 2:51 pm

[…] 未確認リンク Building the perfect iOS Map (II): Completely custom annotation views […]

Jhantelle B
August 15, 2018 At 2:51 pm

Thank you so much for this tutorial! I would’ve spent days working on it by myself.

Leave a Comment

sing in to post your comment or sign-up if you dont have any account.