Blog

Map clustering with Swift – how we implemented it into the ribl iOS app

As we’ve been developing the iPhone version of ribl, we’ve learned a lot about how different iOS and Android are.

In addition to different user interface guidelines and test processes, one of the major variations we found was how Google Maps and Apple Maps handle clustering of pins.

One of the key features of ribl is the Explore feature that allows you to view stories from locations other than your current one. The interface for this feature is a map, and you can move around and zoom in to particular areas to view the stories created there.

The issue arises when there are many stories in a certain area and it becomes difficult to tap a single pin to pull up the associated story.

Google Maps has a native library that provided this map clustering, so we didn’t have to write any additional code for this feature. Apple Maps, on the other hand, didn’t have this library, so we needed to do a bit more work to achieve the map clustering effect.

Here’s how we did it.

With a lot of pins, it’s hard to tap just one

MapKit is a powerful API built into iOS that allows you to drop pins on a native Apple Map. These pins can be tapped to reveal a callout, with options for further action. But for a map-centric app like ribl, it becomes difficult to precisely tap a single pin in a story-dense area.

Map clustering comparison

Left – High pin density makes it difficult to tap an individual pin. Right – Ribl screenshot. Clusters provide a simpler way to represent information.

Other map-centric apps solve this pin-cushion dilemma with map clusters. Real estate apps like RedFin on iOS use clusters. As mentioned earlier, Google Maps has its own map clustering library.

Unfortunately, clustering is not built into MapKit. The third-party map clustering libraries available are written in Objective-C, and we built ribl in Swift, so we needed to use a bridging header to integrate a clustering library into our app.

Maps can be fairly resource-intensive. And as with any other location-based app, maps are a core feature of ribl. For performance and extensibility, it made sense to translate one of these Objective-C libraries to Swift.

How map clustering works

Map clustering relies on something called QuadTree, which is used to subdivide information.

Imagine you have a dresser with four drawers. In the top drawer, you divvy up the left side with skubb boxes from Ikea for your socks and mittens. On the right, maybe you use a Beckis for cuff links and collar stays.

QuadTree works similarly – it repeatedly divides boxes by four until each box contains just a handful of pins. This organization technique is capable of processing thousands of pins with minimal lag.

Quadtree map clustering

Screenshot taken from https://robots.thoughtbot.com/how-to-handle-large-amounts-of-data-on-maps

There are a few Objective-C cluster libraries that use QuadTree. I ultimately decided on translating FBAnnotationClustering (FB stands for Filip Bec, not Facebook) into Swift.

We knew the code base was quality, as it garnered 423 stars and had only 5 open issues on GitHub. In addition, the code base was small, so it would be easier to translate. Win-win.

Translating someone else’s Objective-C code to Swift is painstaking, but it’s a great way to understand the internals of the library. It got tricky when a method call was intertwined with a lot of others, and I often had to stub out dummy methods just to suppress Xcode errors. It reminded me of tracing ethernet cables in my sysadmin days.

One thing I need to remember for next time is to stick to a 1-to-1 translation. Any bright ideas to refactor the code usually ended up in serious regressions. Note to self: the original library is tested code, and every line is there for a good reason.

The ribl Swift clustering library

The final result is this Swift clustering library on our ribl GitHub page. It’s a working example that randomly generates 1000 pins, represented by clusters. Keep zooming in, and the clusters eventually separate out into individual pins.

Instructions for installation and integration into your own project are in the README file. Just copy a few Swift files into a subgroup. Then copy and paste a few snippets of code into your ViewController. If all goes well, you should have two pins in Kentucky that turn into a cluster, depending on your zoom level.

Note that you will need to copy over the image files into your Images.xcassets folder, or the app will crash. It’s a hassle managing image assets, but they do perform better because they’re already rendered. Font Awesome to PNG is a great resource for generating PNGs to fit your project’s color scheme. The icon name is fa-circle, and use 30/60/90, 40/80/120, and 50/100/150px for small, medium, and large clusters, respectively.

Check out this animation of ribl’s map clustering in action:

ribl map clustering on iOS

Conclusion

We hope that this tutorial was helpful! Feel free to use FBAnnotationClusteringSwift in your own project.

To see the end product in the flesh, sign up for our iOS ribl beta and we’ll send you a build via TestFlight.  Or you can see how Google Maps handles clustering by downloading our Android version, which is live on Google Play.

Please let us know in the comments what you think about how we approached the iOS map clustering problem, and how you might implement this solution in your app.

If you liked this article, please share it! 

8 Comments

  1. Chris Ko

    Great work Rob!

  2. mamoun
  3. Jason

    Hi, is it possible to export the clustering algorithm to use a google map instead?

  4. Paul

    I have two data sources, how to remove the annotations when i change data source?

    1. Robert

      Would this work to clear the annotations?

      clusteringManager.setAnnotations([])

  5. Sanjay Mali

    Hello Mike i need help for One of my app that i am using FBAnnotationClusteringSwift which Delegate methods are not calling. When i am adding manually its working.how to make this happen working correctly?

    1. Robert

      For the delegate methods not being called, are you referring to the MapView delegate methods (regionDidChangeAnimated, viewForAnnotation, etc). If so, double-check that the delegate is wired in the Storyboard.

      If you’re referring to the ClusteringManager delegate… Although there’s a line that says `clusteringManager.delegate = self`, this doesn’t really do anything. The clustering manager has a delegate that I think optimizes the cell size based on the map zoom level. I did not enable this feature from the original Objective-C library.

  6. Sean Murphy

    My annotations will merge and un-merge as I zoom out creating a kind of glitchy looking display. Ideally, I’d like the annotations that are within a cluster to remain there as the map zooms out. I’ve messed with several different variable on the FBClusteringManager’s “FBZoomScaleToZoomLevel” switch statement with no luck. Below is what I have to display annotations on the map on my viewcontroller:

    func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    let array: [MKAnnotation] = self.createAnnotationsForVideos(self.mapVideos)
    self.clusteringManager.addAnnotations(array)
    let mapBoundsWidth = Double(self.mapView.bounds.size.width)
    let mapRectWidth: Double = self.mapView.visibleMapRect.size.width
    let scale: Double = mapBoundsWidth / mapRectWidth
    let annotationArray = self.clusteringManager.clusteredAnnotationsWithinMapRect(self.mapView.visibleMapRect, withZoomScale: scale)
    self.clusteringManager.displayAnnotations(annotationArray, onMapView: self.mapView)

    }

Leave a Comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>