Clustering annotations on a map in swift

So the goal of clustering is avoiding having 1000 annotations on the map by grouping them together by area. The more you zoom in, the less clusters you have, until you get to display each single annotation. I tried different libraries and settled for FBAnnotationClusteringSwift as it is written natively in swift.

To install it, just follow the instructions on their github, then feed it your annotations as explained in the documentation. Finally, you might want to add self.mapView.delegate.mapView!(self.mapView, regionDidChangeAnimated: true) in `viewDidLoad` or `viewDidAppear`, otherwise the pin and clusters will appear only when the user start moving the map, thus changing the region!

iOS Simulator Screen Shot 8 août 2015 13.16.46

In case you have some annotations with exactly the same coordinates, the cluster will never separate into distinct annotations are they are exactly on the same spot. One way around it is to check the zoom level, and from very close use the cluster annotation view callback to display the annotations on a list view, for example. For this you can integrate MKMapView category (a category being the objective-c equivalent of an extension in swift). Just download the 2 files, drop them in your project and add #import “MKMapView+ZoomLevel.h” in your bridging header. You can then use the extension easily anywhere in your code like this:

func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
   var reuseId = ""
   if annotation.isKindOfClass(FBAnnotationCluster) {
      reuseId = "Cluster"
      var clusterView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)
      clusterView = FBAnnotationClusterView(annotation: annotation, reuseIdentifier: reuseId)
      if mapView.zoomLevel() >= 15 {
        //here display the annotation view differently 
      }
   }
}

It works nicely, but if like me you have something more than 50 people exactly on the same point it’s not so nice to display a huge list view. So the other option would be to check the coordinates server side, and if some are exactly similar to change them very slightly in order to display them separately on the map. In MYSQL, it would look something like this:

UPDATE `location_data` as t1  #3
INNER JOIN ( #2
    SELECT id 
    FROM `location_data` 
    WHERE 
        entityType = "user" AND 
        CONCAT(lat,";", lng) in 
        (   #1
            SELECT CONCAT(lat,";", lng) 
            FROM `location_data` 
            WHERE entityType = "user" 
            GROUP BY lat, lng 
            HAVING count(*) > 1
        )
)  t2 ON t1.id = t2.id
SET 
    t1.lat = t1.lat+((rand()*2) - 1)/1000, 
    t1.lng = t1.lng+((rand()*2) - 1)/1000

As the function is updating the same table it is selecting I had to use aliases for the table as described on this question on stackoverflow, so it looks a bit complicated but it becomes more clear when you break it up into small chunks:

  1. First we select lat and lng and concatenate them when there a more than 1 result so we can compare them easily
  2. Then we select the id of the result of each row in the previous query
  3. Finally we perform an update on the row by changing slightly the latitude and longitude, by adding a very small random number between -1 and 1, divided by 1000 ( -0,0001 to 0,0001), because 1 degree is about 111km and we don’t want to move the point too much