ClusterMap 是一个用于高性能地图聚类的开源库。
当在地图上显示多个点时,创建聚类是提高用户体验和性能的非常有效的方法。原生聚类机制 提供了一个直接、无需代码的解决方案,并具有视觉上吸引人的动画效果。然而,值得注意的是,这种实现方式确实有一个主要的缺点:所有点都在主线程中添加到地图上,这在处理大量点时可能会导致问题。不幸的是,这个瓶颈是无法避免的。另一个问题是 SwiftUI 仍然缺乏此功能。
ClusterMap 提供了基于 四叉树 算法的高性能计算。这个库是 UI 框架独立的,并在任何线程中执行计算。
ClusterMap 与原生实现在 20,000 个注释上的比较。有关详细比较,请查看 Example-UIKit。
该仓库包含三个主要示例,这是探索库基本功能的良好起点。
将 ClusterMap 依赖项作为包依赖项添加到 Xcode 项目。
将 ClusterMap 依赖项添加到您的 Package.swift
清单中。
dependencies
参数中.package(url: "https://github.com/vospennikov/ClusterMap.git", from: "2.1.0")
.target(
name: "MyTarget",
dependencies: [
.product(name: "ClusterMap", package: "ClusterMap"),
]
)
ClusterManager
存储和聚类地图点。它是一个 actor,并且是线程安全的。
您需要让您的地图点遵循协议 CoordinateIdentifiable, Identifiable, Hashable, Sendable
// SwiftUI annotation
struct ExampleAnnotation: CoordinateIdentifiable, Identifiable, Hashable {
let id = UUID()
var coordinate: CLLocationCoordinate2D
}
// MapKit (MKMapItem) integration
extension MKMapItem: CoordinateIdentifiable, Identifiable, Hashable {
let id = UUID()
var coordinate: CLLocationCoordinate2D {
get { placemark.coordinate }
set(newValue) { }
}
}
// MapKit (MKPointAnnotation) integration
class ExampleAnnotation: MKPointAnnotation, CoordinateIdentifiable, Identifiable, Hashable {
let id = UUID()
}
您需要创建 ClusterManager
的实例,并将您的注释类型设置为替代 ExampleAnnotation
let clusterManager = ClusterManager<ExampleAnnotation>()
接下来,您可以添加和删除您的点。
let reykjavik = ExampleAnnotation(coordinates: CLLocationCoordinate2D(latitude: 64.1466, longitude: -21.9426))
let akureyri = ExampleAnnotation(coordinates: CLLocationCoordinate2D(latitude: 65.6835, longitude: -18.1002))
let husavik = ExampleAnnotation(coordinates: CLLocationCoordinate2D(latitude: 66.0449, longitude: -17.3389))
let cities = [reykjavik, akureyri, husavik]
await clusterManager.add(cities)
await clusterManager.remove(husavik)
await clusterManager.removeAll()
为了正确计算聚类,ClusterMap 需要知道地图的大小。不幸的是,Apple 没有提供此信息。为了读取地图大小,您可以使用 GeometryReader 并将大小传递给您的模型对象。
@State private var mapSize: CGSize = .zero
var body: some View {
GeometryReader(content: { geometryProxy in
Map()
.onAppear(perform: {
mapSize = geometryProxy.size
})
.onChange(of: geometryProxy.size) { oldValue, newValue in
mapSize = newValue
}
})
}
或者您可以使用来自 ClusterMapSwiftUI
包的预构建方法 .readSize
。
import ClusterMapSwiftUI
@State private var mapSize: CGSize = .zero
var body: some View {
Map()
.readSize(onChange: { newValue in
mapSize = newValue
})
}
接下来,您需要每次都将此大小传递给 ClusterMap 的 reload 方法。让我们在每次相机位置改变时重新加载我们的地图。在示例中,我们使用了现代 (iOS 17) 方法 onMapCameraChange。与 iOS 17 之前的 Map 集成有点棘手,有关更多信息,请查看 Example-SwiftUI/App/LegacyMap
Map()
.onMapCameraChange(frequency: .onEnd) { context in
Task.detached { await clusterManager.reload(mapViewSize: mapSize, coordinateRegion: context.region) }
}
作为此方法的结果,ClusterMap 返回结构体 ClusterManager<ExampleAnnotation>.Difference
。您需要将此差异应用于您的视图。
private var annotations: [ExampleAnnotation] = []
private var clusters: [ExampleClusterAnnotation] = []
func applyChanges(_ difference: ClusterManager<ExampleAnnotation>.Difference) {
for removal in difference.removals {
switch removal {
case .annotation(let annotation):
annotations.removeAll { $0 == annotation }
case .cluster(let clusterAnnotation):
clusters.removeAll { $0.id == clusterAnnotation.id }
}
}
for insertion in difference.insertions {
switch insertion {
case .annotation(let newItem):
annotations.append(newItem)
case .cluster(let newItem):
clusters.append(ExampleClusterAnnotation(
id: newItem.id,
coordinate: newItem.coordinate,
count: newItem.memberAnnotations.count
))
}
}
}
有关集成的更多信息,请查看 Example-SwiftUI
为了正确计算聚类,ClusterMap 需要知道地图的大小。我们可以直接从 MKMapView
读取大小。
var mapView = MKMapView(frame: .zero)
func reloadMap() async {
await clusterManager.reload(mapViewSize: mapView.bounds.size, coordinateRegion: mapView.region)
}
作为此方法的结果,ClusterMap 返回结构体 ClusterManager<ExampleAnnotation>.Difference
。您需要将此差异应用于您的视图。这很棘手,因为您的 MKMapView 保留了 MKAnnotation
并擦除了类型。您可以将注释转换为类型检查,或者将它们保存在额外的变量中。为了清晰的示例,我将它们保存在变量中。
var annotations: [ExampleAnnotation] = []
private func applyChanges(_ difference: ClusterManager<ExampleAnnotation>.Difference) {
for annotationType in difference.removals {
switch annotationType {
case .annotation(let annotation):
annotations.removeAll(where: { $0 == annotation })
mapView.removeAnnotation(annotation)
case .cluster(let clusterAnnotation):
if let result = annotations.enumerated().first(where: { $0.element.id == clusterAnnotation.id }) {
annotations.remove(at: result.offset)
mapView.removeAnnotation(result.element)
}
}
}
for annotationType in difference.insertions {
switch annotationType {
case .annotation(let annotation):
annotations.append(annotation)
mapView.addAnnotation(annotation)
case .cluster(let clusterAnnotation):
let cluster = ClusterAnnotation()
cluster.id = clusterAnnotation.id
cluster.coordinate = clusterAnnotation.coordinate
cluster.memberAnnotations = clusterAnnotation.memberAnnotations
annotations.append(cluster)
mapView.addAnnotation(cluster)
}
}
}
有关集成的更多信息,请查看 Example-UIKit
ClusterMap 是一个 UI 框架独立的库。您可以将此库集成到任何应用层。让我们看看如何将此库作为 TCA 依赖项集成。最后,您可以将结果保存在数据库中,并在任何时候重用项目,而无需额外的重量级计算。
struct ClusterMapClient {
struct ClusterClientResult {
let objects: [ExampleAnnotation]
let clusters: [ExampleClusterAnnotation]
}
var clusterObjects: @Sendable ([ExampleAnnotation], MKCoordinateRegion, CGSize) async -> ClusterClientResult
}
extension DependencyValues {
var clusterMapClient: ClusterMapClient {
get { self[ClusterMapClient.self] }
set { self[ClusterMapClient.self] = newValue }
}
}
extension ClusterMapClient: DependencyKey {
static var liveValue = ClusterMapClient(
clusterObjects: { inputObjects, mapRegion, mapSize in
let clusterManager = ClusterManager<ExampleAnnotations>()
await clusterManager.add(inputObjects)
await clusterManager.reload(mapViewSize: mapSize, coordinateRegion: mapRegion)
var objects: [ExampleAnnotation] = []
var clusters: [ExampleClusterAnnotation] = []
await clusterManager.visibleAnnotations.forEach { annotationType in
switch annotationType {
case .annotation(let annotation):
objects.append(annotation)
case .cluster(let cluster):
clusters.append(.init(coordinate: cluster.coordinate))
}
}
return .init(objects: objects, clusters: clusters)
}
)
}
ClusterManager
具有配置,可帮助您提高性能和控制聚类逻辑,有关更多信息,请查看 ClusterManager.Configuration
有关发布版本和 main
的文档可在此处找到
这个项目基于 Lasha Efremidze 的工作,他创建了 Cluster。
此库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。