Skip to content

📍 MapMetrics iOS Map Features Guide

This guide covers how to:

Add Clusters

🔧 Prerequisites

Xcode installed (version 13 or newer recommended) A new or existing iOS project Add MapMetrics SDK to your project (via SPM or manual integration)

Example via Swift Package Manager:

: https://github.com/MapMetrics/MapMetrics-iOS

1️⃣ Setting Up the Map

In your ViewController.swift:

swift
class ViewController: UIViewController, MLNMapViewDelegate {
    var mapView: MLNMapView!
    var selectedAnnotation: MLNPointAnnotation?
    var isMarkerSelected = false

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView = MLNMapView(
            frame: view.bounds,
            styleURL: URL(string: "<YOUR_STYLE_URL_HERE>")
        )
        mapView.delegate = self
        view.addSubview(mapView)
                mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self
        
        // This is a better starting position 
        let center = CLLocationCoordinate2D(latitude: 20.0, longitude: 0.0) // More centered globally
        mapView.setCenter(center, zoomLevel: 2, animated: false) // Lower zoom to see more area
        view.addSubview(mapView)

    }
}

Displaying Clusters from GeoJSON

###Step 1: Create the source

swift
let url = URL(string: "https://cdn.mapmetrics-atlas.net/Images/heatmap.geojson")!
let source = MLNShapeSource(
    identifier: "clusteredEarthquakes",
    url: url,
    options: [
        .clustered: true,
        .clusterRadius: 30
    ]
)
style.addSource(source)

Step 2: Add the cluster layers

swift
private func addClusterLayers(source: MLNShapeSource, to style: MLNStyle) throws {
        // Cluster layer
        let clusterLayer = MLNCircleStyleLayer(identifier: "clusters", source: source)
        clusterLayer.circleColor = NSExpression(format: "mgl_step:from:stops:(point_count, %@, %@)",
                                                UIColor.systemTeal,
                                                [100: UIColor.systemYellow, 750: UIColor.systemPink])
        clusterLayer.circleRadius = NSExpression(format: "mgl_step:from:stops:(point_count, 20, %@)",
                                                 [100: 30, 750: 40])
        clusterLayer.predicate = NSPredicate(format: "point_count >= 0")
        style.addLayer(clusterLayer)
        
        // Count layer
        let countLayer = MLNSymbolStyleLayer(identifier: "cluster-count", source: source)
        countLayer.text = NSExpression(format: "CAST(point_count_abbreviated, 'NSString')")
        countLayer.textFontNames = NSExpression(forConstantValue: ["Noto Sans Medium"])
        countLayer.textFontSize = NSExpression(forConstantValue: 12)
        countLayer.predicate = NSPredicate(format: "point_count >= 0")
        style.addLayer(countLayer)
        
        // Unclustered point layer
        let pointLayer = MLNCircleStyleLayer(identifier: "unclustered-point", source: source)
        pointLayer.circleColor = NSExpression(forConstantValue: UIColor.systemBlue)
        pointLayer.circleRadius = NSExpression(forConstantValue: 4)
        pointLayer.circleStrokeWidth = NSExpression(forConstantValue: 1)
        pointLayer.circleStrokeColor = NSExpression(forConstantValue: UIColor.white)
        pointLayer.predicate = NSPredicate(format: "point_count == nil")
        style.addLayer(pointLayer)
    }

Complete Setup

swift
    func setupClusters() {
        print("🟢 Starting cluster setup")
        
        guard let style = mapView.style else {
            print("🔴 CRITICAL ERROR: Map style is nil")
            return
        }
        
        guard let url = URL(string: "https://cdn.mapmetrics-atlas.net/Images/heatmap.geojson") else {
            print("🔴 ERROR: Invalid GeoJSON URL")
            return
        }
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                print("✅ GeoJSON Size: \(data.count) bytes")
                if let geoJSON = String(data: data, encoding: .utf8) {
                    print("📦 Preview GeoJSON:\n\(geoJSON.prefix(500))")
                }
            } else {
                print("❌ Failed to fetch GeoJSON: \(error?.localizedDescription ?? "Unknown error")")
            }
        }
        task.resume()
        if let layer = style.layer(withIdentifier: "earthquakes-heat") as? MLNVectorStyleLayer {
            print("Heatmap filter: \(String(describing: layer.predicate))")
        }


        do {
            // Create source without custom cluster properties first
            let source = MLNShapeSource(
                identifier: "clusteredEarthquakes",
                url: url,
                options: [
                    .clustered: true,
                    .clusterRadius: 30
                ]
            )
            if let shapeCollection = source.shape as? MLNShapeCollectionFeature {
                var minLat = 90.0
                var maxLat = -90.0
                var minLon = 180.0
                var maxLon = -180.0

                for feature in shapeCollection.shapes {
                    if let point = feature as? MLNPointFeature {
                        let coord = point.coordinate
                        minLat = min(minLat, coord.latitude)
                        maxLat = max(maxLat, coord.latitude)
                        minLon = min(minLon, coord.longitude)
                        maxLon = max(maxLon, coord.longitude)
                    }
                    // Optionally handle more geometry types like MGLPolylineFeature or MGLPolygonFeature here
                }

                let sw = CLLocationCoordinate2D(latitude: minLat, longitude: minLon)
                let ne = CLLocationCoordinate2D(latitude: maxLat, longitude: maxLon)
                let bounds = MLNCoordinateBounds(sw: sw, ne: ne)

                let camera = mapView.cameraThatFitsCoordinateBounds(bounds, edgePadding: .init(top: 40, left: 20, bottom: 40, right: 20))
                mapView.setCamera(camera, animated: true)
            }


            
            style.addSource(source)
            print("🟢 Source added successfully")
            
            // Create and add layers...
            try addClusterLayers(source: source, to: style)
            
        } catch {
            print("🔴 ERROR: \(error.localizedDescription)")
            if let nsError = error as NSError? {
                print("User Info: \(nsError.userInfo)")
            }
        }
    }

    private func addClusterLayers(source: MLNShapeSource, to style: MLNStyle) throws {
        // Cluster layer
        let clusterLayer = MLNCircleStyleLayer(identifier: "clusters", source: source)
        clusterLayer.circleColor = NSExpression(format: "mgl_step:from:stops:(point_count, %@, %@)",
                                                UIColor.systemTeal,
                                                [100: UIColor.systemYellow, 750: UIColor.systemPink])
        clusterLayer.circleRadius = NSExpression(format: "mgl_step:from:stops:(point_count, 20, %@)",
                                                 [100: 30, 750: 40])
        clusterLayer.predicate = NSPredicate(format: "point_count >= 0")
        style.addLayer(clusterLayer)
        
        // Count layer
        let countLayer = MLNSymbolStyleLayer(identifier: "cluster-count", source: source)
        countLayer.text = NSExpression(format: "CAST(point_count_abbreviated, 'NSString')")
        countLayer.textFontNames = NSExpression(forConstantValue: ["Noto Sans Medium"])
        countLayer.textFontSize = NSExpression(forConstantValue: 12)
        countLayer.predicate = NSPredicate(format: "point_count >= 0")
        style.addLayer(countLayer)
        
        // Unclustered point layer
        let pointLayer = MLNCircleStyleLayer(identifier: "unclustered-point", source: source)
        pointLayer.circleColor = NSExpression(forConstantValue: UIColor.systemBlue)
        pointLayer.circleRadius = NSExpression(forConstantValue: 4)
        pointLayer.circleStrokeWidth = NSExpression(forConstantValue: 1)
        pointLayer.circleStrokeColor = NSExpression(forConstantValue: UIColor.white)
        pointLayer.predicate = NSPredicate(format: "point_count == nil")
        style.addLayer(pointLayer)
    }