📍 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)
}