This is a Swift translation of FBAnnotationClustering. Aggregates map pins into a single numbered cluster.
Map clustering is a common enough map feature in modern apps. When I couldn't find a Swift library, I ended up translating one from Objective-C. The library of choice was FBAnnotationClustering (FB stands for Filip Bec, not Facebook). I wanted something that was fast (QuadTree), with a light code base in case I had to figure out and troubleshoot an edge case down the road.
(left: sample project with a lot of pins in the DC area. right: ribl screenshot using clusters)
pod 'FBAnnotationClusteringSwift'
and in class where do you need add this
import FBAnnotationClusteringSwift
Copy the following Swift files to your project:
- FBClusteringManager.swift
- FBAnnotation.swift
- FBAnnotationCluster.swift
- FBAnnotationClusterTemplate.swift
- FBAnnotationClusterView.swift
- FBAnnotationClusterViewConfiguration.swift
- FBAnnotation.swift
- FBQuadTree.swift
- FBQuadTreeNode.swift
- FBBoundingBox.swift
- FBBoundingBox+MapKit.swift
Use FBViewController.swift as a guide. For demonstration purposes, it drops 1000 random pins near Ghana.
Follow instructions below for a barely-working implementation.
let clusteringManager = FBClusteringManager()
var array:[FBAnnotation] = []
// drop two arbitrary pins somewhere near Louisville, Kentucky
let pinOne = FBAnnotation()
pinOne.coordinate = CLLocationCoordinate2D(latitude: 38.188805, longitude: -85.6767705)
let pinTwo = FBAnnotation()
pinTwo.coordinate = CLLocationCoordinate2D(latitude: 38.188806, longitude: -85.6767707)
array.append(pinOne)
array.append(pinTwo)
clusteringManager.add(annotations: array)
Add this to the top of your ViewController:
import MapKit
Add a MapKit View in the Storyboard, and set the delegate.
Drop in these MKMapViewDelegate methods:
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
DispatchQueue.global(qos: .userInitiated).async {
let mapBoundsWidth = Double(self.mapView.bounds.size.width)
let mapRectWidth = self.mapView.visibleMapRect.size.width
let scale = mapBoundsWidth / mapRectWidth
let annotationArray = self.clusteringManager.clusteredAnnotations(withinMapRect: self.mapView.visibleMapRect, zoomScale:scale)
DispatchQueue.main.async {
self.clusteringManager.display(annotations: annotationArray, onMapView:self.mapView)
}
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var reuseId = ""
if annotation is FBAnnotationCluster {
reuseId = "Cluster"
var clusterView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
if clusterView == nil {
clusterView = FBAnnotationClusterView(annotation: annotation, reuseIdentifier: reuseId, configuration: FBAnnotationClusterViewConfiguration.default())
} else {
clusterView?.annotation = annotation
}
return clusterView
} else {
reuseId = "Pin"
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView?.pinTintColor = UIColor.green
} else {
pinView?.annotation = annotation
}
return pinView
}
}
}
If you want run example, before to open Xcode the first time, open Terminal in Example folder and run the command:
pod install
After than open the .xworkspace file generated.
Cluster's range and appearance are fully customizable via the FBAnnotationClusterViewConfiguration class. Each range have his own template (FBAnnotationClusterTemplate) allowing each segment to look different. You can create as many templates you like as long the ranges don't overlap each others. Each template can be either displayed as a circle with a solid color and a stroke or as an image.
Just take a look at FBAnnotationClusterViewConfiguration.default() function.
TODO
TODO