Building an iOS Clustered Map View

Want to implement a map interface in your app? Don’t settle for just a basic boring MKMapView…

Instead this tutorial will show you how to build a dynamic map that offers automatic real-time fetching when the map moves and clustering of annotations:

Cover image of the ios clustered map view in action

Overview

Traditionally, building a dynamic map like the one above meant creating your own specialized data structure, such as a quad tree. This is difficult to build and comes with other drawbacks such as memory management and re-indexing to changes. Instead, Realm allows us to skip all that complexity and take advantage of its incredible speed.

All that is needed is to store your location data in Realm and utilize RealmMapView in the same way you would use MKMapView.

Let’s get started, but if you want to jump to the end product, you can see the final code on GitHub here.

If you are looking for an Objective-C version, you can check out the tutorial for ABFRealmMapView, which has an identical API.

Tutorial

Create a new Xcode project, using the “Single View Application” template. Be sure “Language” is set to Swift, and that “Use Core Data” is unchecked.

RealmMapView is available through CocoaPods. (If you don’t have CocoaPods installed, follow their instructions before continuing.) In your terminal, cd to the project folder you just created, and open the Podfile (if you don’t have a podfile, perform pod init in the folder first). To install RealmMapView, simply add the following lines to the Podfile you just created:

use_frameworks!

pod 'RealmMapView'

# Map data set
pod 'RealmSwiftSFRestaurantData'

In your terminal, run pod install. This will also install Realm automatically! (If this is your first time using CocoaPods, this could take a while. Future uses will be much faster.)

When it’s done, you’ll need to close the Xcode window and open the xcworkspace file that CocoaPods created, so that you can use the classes inside the pods.

In main.storyboard, add an MKMapView to the view controller, and change its “Class” (in the Identity Inspector, at the top right) to RealmMapView (which is a subclass of MKMapView).

Now we need some data to put on the map! For this tutorial, we’ll be using some restaurant data. Recall, we already installed via CocoaPods RealmSwiftSFRestaurantData. This includes the locations of all the restaurants in the city of San Francisco.

To make RealmMapView aware of which data to display, you need to give it the Realm object name and key paths for latitude, longitude, and annotation view title and subtitle. You can change these properties in Interface Builder, as shown in the image below (you can also set the properties programmatically too).

Property Value
Entity Name ABFRestaurantObject
Latitude Key Path latitude
Longitude Key Path longitude
Title Key Path name
Subtitle Key Path phoneNumber

Now for this data set we need to do a bit more configuration since the data is stored in a secondary Realm (rather than the default Realm).

In ViewController.m, add this header to the top of the file:

import RealmSwift
import RealmSwiftSFRestaurantData
import RealmMapView

Add an IBOutlet for the RealmMapView from your storyboard to this view controller.

As mentioned above, we need to configure RealmMapView to use the path to the separate Realm file with the restaurant data. Luckily, RealmSwiftSFRestaurantData makes this easy by supplying a function to retrieve the path.

In the same file, add the following lines after super.viewDidLoad():

var config = Realm.Configuration.defaultConfiguration
config.path = ABFRestaurantScoresPath()
self.mapView.realmConfiguration = config

When that’s done, your ViewController.swift file should look like this:

import UIKit
import MapKit
import RealmSwift
import RealmSwiftSFRestaurantData

class ViewController: UIViewController {

    @IBOutlet var mapView: RealmMapView!

    override func viewDidLoad() {
        super.viewDidLoad()

        var config = Realm.Configuration.defaultConfiguration
        config.path = ABFRestaurantScoresPath()
        self.mapView.realmConfiguration = config
    }
}

That’s it! Run your app and enjoy the restaurants of San Francisco.

Extras

Want to add a little more functionality to understand some more of the capabilities RealmMapView offers?

If you notice, by default, clicking on a cluster annotation reveals a callout that says “X objects in this area”. Our example, though is displaying restaurants, so it would be great if this read “X restaurants in this area”.

RealmMapView offers this functionality from its internal fetchedResultsController, via a property: clusterTitleFormatString.

This property accepts a string that represents the annotation callout’s title string. To insert the number of items, the string must use the $OBJECTSCOUNT variable somewhere in the string which will be replaced by the object count.

So to use this in the example just add this line in ViewController.swift in viewDidLoad():

self.mapView.fetchedResultsController.clusterTitleFormatString = "$OBJECTSCOUNT restaurants in this area"

Now run your app and click on a cluster to see the change in the title!

Set the map view's properties

The other feature that you will likely want is the ability to retrieve the object or in the case of a cluster, objects, that are associated with the annotation.

MKMapView offers a delegate callback whenever a user selects an annotation view via:

func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView)

Let’s first get this setup, then we can see how to retrieve the object in the function. Go back to ViewController.swift and add this line in viewDidLoad():

self.mapView.delegate = self

This sets the view controller as the delegate, but now we need to actually conform to the protocol. This is easy to do via a class extension:

extension ViewController: MKMapViewDelegate {
    func mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView) {
        if let safeObjects = ABFClusterAnnotationView.safeObjectsForClusterAnnotationView(view) {

          if let firstObjectName = safeObjects.first?.toObject(ABFRestaurantObject).name {
              print("First Object: \(firstObjectName)")
          }
        }
    }
}

In the code sample above, this demonstrates how you can retrieve the objects tied to the annotation. ABFClusterAnnotationView as a class method: safeObjectsForClusterAnnotationView which returns an array of ABFLocationSafeRealmObjects. These objects are intermediate representations of the original Realm object, and are thread-safe. To convert the safe object back into the original object, just call toObject(Object.Type). In the example above, we retrieve the first safe object and convert it back into a ABFRestaurantObject.



Realm Cocoa Team

Realm Cocoa Team