I have a MKMapView that is being handled with a SwiftUI UIViewRepresentable
. The problem is that my clustering is failing to work as expected. My code produces the following series of problems.
- Map loads, clusterable annotations cluster as expected.
- Zooming in on a cluster de-clusters them.
- Zooming out, they re-cluster, however if I zoom out even further, additional annotions are not clusterd with the group.
- Zooming back in after encountering this bug, results in no more clustering of any kind.
Cluster Annotation View
final class ClusterLocationAnnotationView: MKAnnotationView {
static let identifier = "cluster-location-annotation-identifier"
// Omitted irrelevant `UILabel` and other views.
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .defaultHigh
collisionMode = .rectangle
clusteringIdentifier = "location-detail-cluster-id"
canShowCallout = false
setupUI()
}
// Removed Required Init
private func setupUI() {
addSubview(backgroundView)
addSubview(label)
addSubview(offersLabel)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor)
])
offersLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
offersLabel.topAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: 4), // adjust the constant as needed
offersLabel.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor)
])
}
// MARK: - Configure Cluster
func configure(appConfig: AppConfig, clusterLocations: [LocationDetail]) {
backgroundView.backgroundColor = UIColor(cgColor: appConfig.secondaryColor.cgColor ?? UIColor.black.cgColor)
backgroundView.layer.cornerRadius = appConfig.globalCornerRadii
label.text = clusterLocations.count.description
label.textColor = UIColor(cgColor: appConfig.secondaryColor.contrastingColor().cgColor ?? UIColor.white.cgColor)
// Set number of locationDetails that contain an offer, to the text, eg. offersLabel.text = locationsWithOffersCount
var offerCount = 0
for location in clusterLocations {
offerCount += appConfig.hasOffer(for: location) ? 1 : 0
}
offersLabel.text = "(offerCount) Offer(s) Available"
}
Annotation View
final class LocationAnnotationView: MKAnnotationView {
static let identifier = "location-annotation-identifier"
// Removed irrelevant views.
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .defaultHigh
collisionMode = .rectangle
clusteringIdentifier = "location-detail-cluster-id"
canShowCallout = true
setupUI()
}
//Removed Required INIT
private func setupUI() {
addSubview(backgroundView)
addSubview(imageView)
addSubview(label)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
backgroundView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
backgroundView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
backgroundView.widthAnchor.constraint(equalToConstant: 40),
backgroundView.heightAnchor.constraint(equalToConstant: 40)
])
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor),
imageView.widthAnchor.constraint(equalToConstant: 30),
imageView.heightAnchor.constraint(equalToConstant: 30)
])
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: 4),
label.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor)
])
}
func configure(with location: LocationDetail, and category: Category, appConfig: AppConfig) {
backgroundView.backgroundColor = UIColor(cgColor: appConfig.secondaryColor.cgColor ?? UIColor.black.cgColor)
backgroundView.layer.cornerRadius = appConfig.globalCornerRadii
imageView.image = Iconoir.uiImage(from: category.iconName)?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = UIColor(cgColor: appConfig.secondaryColor.contrastingColor().cgColor ?? UIColor.white.cgColor)
self.label.text = location.locationName
}
}
Coordinator MKMapViewDelegate Functions
func mapView(_ mapView: MKMapView, clusterAnnotationForMemberAnnotations memberAnnotations: [MKAnnotation]) -> MKClusterAnnotation {
return MKClusterAnnotation(memberAnnotations: memberAnnotations)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let appConfig = parent.appConfig
if let cluster = annotation as? MKClusterAnnotation {
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: ClusterLocationAnnotationView.identifier, for: cluster) as? ClusterLocationAnnotationView
annotationView?.frame.size.height = 40
annotationView?.frame.size.width = 40
annotationView?.configure(appConfig: appConfig, clusterLocations: cluster.memberAnnotations as! [LocationDetail])
return annotationView
} else if let location = annotation as? LocationDetail {
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: LocationAnnotationView.identifier, for: annotation) as? LocationAnnotationView
annotationView?.frame.size.height = 40
annotationView?.frame.size.width = 40
let category = appConfig.category(from: location.categoryId)
annotationView?.configure(with: location, and: category, appConfig: appConfig)
return annotationView
}
return nil
}
Update UIView
func updateUIView(_ mapView: MKMapView, context: Context) {
let coordinator = context.coordinator
mapView.mapType = mapType
mapView.userTrackingMode = trackingMode
handleLocationChange(mapView, coordinator: coordinator)
}
func handleLocationChange(_ mapView: MKMapView, coordinator: Coordinator) {
if coordinator.lastLocations.count != locations.count {
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(locations)
mapView.setRegion(getRegion(for: locations), animated: true)
coordinator.lastLocations = locations
DispatchQueue.main.async {
trackingMode = .none
}
}
}
2
Answers
A few issues needed to be resolved. Firstly, I subclassed
MKMapView
on myCoordinator
, I don't think this is truly needed as I have access to themapView
instance inupdateUIView
.I needed to update my
makeUIView
to properly update based on the updated state. Previously, I was mishandling my@State
and both updating@State
from themakeUIView
and from theUIViewRepresentable
struct. The general rule here is that any value that is update fromUIViewRepresentable
, aka aState
value is published from SwiftUI, it should update the value on theCoordinator
. Any value that theCoordinator
updates, should be published to theState
variable in theUIViewRepresentable
struct. That will in turn, cause theupdateUIView(..)
function to be called, updating theCoordinator
's property value with the newly updatedState
value.To solve the
MKAnnotation
clustering issue I checked my code for theclusteringIdentifier
and I had aclusteringIdentifier
on myLocationDetail
as well as set onLocationAnnotationView
and on myClusterLocationAnnotationView
so I removed that value and am only using it when theviewForAnnotation(:)
is called.handleLocationChange needs to be moved into the coordinator so it can be called in the map region changed event.
Also perhaps remove the location count check, it looks error prone.