ClusterMap is an open-source library for high-performance map clustering. When you want to present many points over the map for better user experience and performance, you can make clusters from many points. A good starting point is using a native clustering mechanism. It's simple to implement, simple to use, and has nice animations with zero lines of code. The core problem of the native implementation is adding all points to the map in MainThread. You can't avoid this bottleneck, and it's a problem if you want to operate with thousand points. ClusterMap uses efficient QuadTree storage and performs all computations in the background thread.
Comparison with 20,000 annotations. For a detailed comparison, use Example-UIKit.
- UIKit
- SwiftUI
- Swift concurrency
- Adding/Removing Annotations
- Clustering Annotations
- Multiple Managers
- Dynamic Cluster Disabling
- Custom Cell Size
- Custom Annotation Views
The Example is a great place to start. It demonstrates how to:
- Integrate the library
- Add/remove annotations
- Reload annotations
- Configure the annotation view
- Configure the manager
- Compare Apple's native implementation with to library
ClusterMap is available via Swift Package Manager.
Add the following dependency to your Package.swift file:
.package(url: "https://github.com/vospennikov/ClusterMap.git", from: "1.1.0")
ClusterMap is available through CocoaPods. To install it, add the following line to your Podfile:
pod 'ClusterMap', '1.1.0'
The ClusterManager
class generates, manages, and displays annotation clusters.
let clusterManager = ClusterManager()
Create an object that conforms to the MKAnnotation
protocol. Next, add the annotation object to an instance of ClusterManager
with add(annotation:)
.
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: 21.283921, longitude: -157.831661)
manager.add(annotation)
Implement the map view's mapView(_:viewFor:)
delegate method to configure the annotation view. Return an instance of MKAnnotationView
to visually represent the annotations.
To display clusters, return an instance of ClusterAnnotationView
.
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? ClusterAnnotation {
return CountClusterAnnotationView(annotation: annotation, reuseIdentifier: "cluster")
} else {
return MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
}
}
}
For performance reasons, you should generally reuse MKAnnotationView
objects in your map views. See the Example to learn more.
The ClusterAnnotationView
class exposes a countLabel
property. You can subclass ClusterAnnotationView
to provide custom behavior as needed. Here's an example of subclassing the ClusterAnnotationView
and customizing the layer borderColor
.
class CountClusterAnnotationView: ClusterAnnotationView {
override func configure(_ annotation: ClusterAnnotation) {
super.configure(annotation)
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = 1.5
}
}
See the AnnotationView to learn more.
To remove annotations, you can call remove(annotation:)
. However, the annotations will still display until you call reload()
.
manager.remove(annotation)
If shouldRemoveInvisibleAnnotations
is set to false
, annotations that have been removed may still appear on the map until calling reload()
on the visible region.
Implement the map view's mapView(_:regionDidChangeAnimated:)
delegate method to reload the ClusterManager
when the region changes.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
clusterManager.reload(mapView: mapView) { finished in
// handle completion
}
}
You should call reload()
anytime you add or remove annotations.
The ClusterManager
class exposes several properties to configure clustering:
var zoomLevel: Double // The current zoom level of the visible map region.
var maxZoomLevel: Double // The maximum zoom level before disabling clustering.
var minCountForClustering: Int // The minimum number of annotations for a cluster. The default is `2`.
var shouldRemoveInvisibleAnnotations: Bool // Whether to remove invisible annotations. The default is `true`.
var shouldDistributeAnnotationsOnSameCoordinate: Bool // Whether to arrange annotations in a circle if they have the same coordinate. The default is `true`.
var distanceFromContestedLocation: Double // The distance in meters from contested location when the annotations have the same coordinate. The default is `3`.
var clusterPosition: ClusterAlignment // The position of the cluster annotation. The default is `.nearCenter`.
The ClusterManagerDelegate
protocol provides many functions to manage clustering and configure cells.
// The size of each cell on the grid at a given zoom level.
func cellSize(for zoomLevel: Double) -> Double? { ... }
// Whether to cluster the given annotation.
func shouldClusterAnnotation(_ annotation: MKAnnotation) -> Bool { ... }
The documentation for releases and main
are available here:
This project is based on the work of Lasha Efremidze, who created the Cluster.
ClusterMap is available under the MIT license. See the LICENSE file for more info.