Reputation: 2472
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.
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"
}
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
}
}
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
}
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
}
}
}
Upvotes: 1
Views: 179
Reputation: 2472
A few issues needed to be resolved. Firstly, I subclassed MKMapView
on my Coordinator
, I don't think this is truly needed as I have access to the mapView
instance in updateUIView
.
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 the makeUIView
and from the UIViewRepresentable
struct. The general rule here is that any value that is update from UIViewRepresentable
, aka a State
value is published from SwiftUI, it should update the value on the Coordinator
. Any value that the Coordinator
updates, should be published to the State
variable in the UIViewRepresentable
struct. That will in turn, cause the updateUIView(..)
function to be called, updating the Coordinator
's property value with the newly updated State
value.
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
mapView.userTrackingMode = trackingMode
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(locations)
}
To solve the MKAnnotation
clustering issue I checked my code for the clusteringIdentifier
and I had a clusteringIdentifier
on my LocationDetail
as well as set on LocationAnnotationView
and on my ClusterLocationAnnotationView
so I removed that value and am only using it when the viewForAnnotation(:)
is called.
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])
annotationView?.clusteringIdentifier = "clustering-location-detail-identifier"
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)
annotationView?.clusteringIdentifier = "clustering-location-detail-identifier"
return annotationView
}
return nil
}
Upvotes: 0
Reputation: 30746
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.
Upvotes: 0