Location Clustering for iOS

Last updated:

  1. Developing on the new Arm-based Apple Silicon (M1) Macs requires building and running on a physical iOS device or using an iOS simulator running iOS 13.7, e.g. iPhone 11. This is a temporary limitation in Google Maps SDK for iOS, and as such also a limitation in MapsIndoors, due to the dependency to Google Maps.
  2. Note: Due to a bug in CocoaPods it is necessary to include the post_install hook in your Podfile described in the PodFile post_install wiki.

This is an example of enabling and disabling location grouping on the map as well as providing custom cluster tapping behavior and custom cluster images.

Start by creating a UIViewController class that conforms to the MPMapControlDelegate protocol

class ClusteringController: UIViewController, MPMapControlDelegate {

Add a GMSMapView and a MPMapControl to the class Also define a clustering enabling/disabling button and a dictionary to store the clustering images for reuse

var map: GMSMapView? = nil
var mapControl: MPMapControl? = nil
let clusteringButton = UIButton.init()
var clusteringImageDictionary = Dictionary<String, UIImage>()

Setup map so that it shows the demo venue and initialise mapControl

self.map = GMSMapView.init(frame: CGRect.zero)
self.map?.camera = .camera(withLatitude: 57.057964, longitude: 9.9504112, zoom: 20)
self.mapControl = MPMapControl.init(map: self.map!)
self.mapControl?.delegate = self

Setup a button that enables/disables the location grouping / clustering mechanism

clusteringButton.setTitle("Clustering disabled", for: .normal)
clusteringButton.setTitle("Clustering enabled", for: .selected)
clusteringButton.addTarget(self, action: #selector(toggleClustering), for: .touchUpInside)
clusteringButton.backgroundColor = UIColor.blue

Arrange the map view and the button in a stackview

let stackView = UIStackView.init(arrangedSubviews: [map!, clusteringButton])
stackView.axis = .vertical
view = stackView

Define an objective-c method toggleClustering that will receive events from your button, and toggle the clustering flag:

  • Check current state
  • Swap state
  • Make button reflect the state
@objc func toggleClustering() {
if MPMapControl.locationClusteringEnabled {
MPMapControl.locationClusteringEnabled = false
} else {
MPMapControl.locationClusteringEnabled = true
clusteringButton.isSelected = MPMapControl.locationClusteringEnabled

Define the delegate method didTap that will receive tap events from a cluster marker

  • Check if zoom is possible and increment map zoom
  • Return true to indicate that you handle the event and do not want default behavior to happen
func didTap(_ marker: GMSMarker, forPoiGroup locations: [MPLocation]?, moreZoomPossible: Bool) -> Bool {
if moreZoomPossible {
self.map?.animate(toZoom: self.map!.camera.zoom + 1)
return true

Define the delegate method getImageSizeForPoiGroup that provides the size of the potential cluster

  • Check if zoom is possible and increment map zoom
  • Return true to indicate that you handle the event and do not want default behavior to happen
func getImageSizeForPoiGroup(withCount count: UInt, clusterId: String) -> CGSize {
let width = 48 * (Int(log10(Double(count))) + 1)
let height = 48
return CGSize.init(width: width, height: height)

Define the delegate method getImageForPoiGroup that asynchronously provides the image of the potential cluster

func getImageForPoiGroup(_ poiGroup: [MPLocation], imageSize: CGSize, clusterId: String, completion: @escaping (UIImage?) -> Void) -> Bool {

In getImageForPoiGroup create a string hash for the image

let imgHash = "img\(poiGroup.count)\(clusterId)"

In getImageForPoiGroup check if image already exists. If image does not exist, go in a background thread to get a dummy image and call the completion handler. Return true to indicate that you handle the clustering image.

var img = clusteringImageDictionary[imgHash]
if img == nil {
DispatchQueue.global().async {
let imgUrlString = "https://placem.at/people?txt=\(poiGroup.count)&random=\(Int.random(in: 0 ..< 10))&w=\(imageSize.width*2)&h=\(imageSize.height*2)"
let imgUrl = URL(string: imgUrlString)
do {
let imgData = try Data.init(contentsOf: imgUrl!)
img = UIImage(data: imgData, scale: 2)
self.clusteringImageDictionary[imgHash] = img
} catch {
} else {
return true

See the sample in ClusteringController.swift