I'm struggling to get KVO updates within a callout already displayed.
My use case: I want to display on an open callout the real time distance between user location and the annotation I add to the map. Annotation does not change its position.
- I add annotations to mapView, using a custom annotation I have defined. No issue here.
- On each annotation selected, the callout displays all the information defined in the custom annotation
- However, the distance is refreshed in the callout ONLY if I unselect the annotation and reselect it
The distance property is declared as @objc dynamic so it can be observed. I compute the distance each time the user location change. This part works too.
I cannot figure out what I'm missing to have the callout updated without closing and reopening it.
The code I'm using is what is described here by Rob: Swift -How to Update Data in Custom MKAnnotation Callout?
So my question: is it possible to change realtime a value (observed) in a notificationView callout ? If yes is KVO the best approach ? In the link below, how would be implemented the mapView viewFor method ?
Any example would be very helpful.
It's my first post here, so please if I did it wrong, let me know and I will provide more information and details. But my situation is trivial: the standard callout performs Key-Value Observation (KVO) on title and subtitle. (And the annotation view observes changes to coordinate.). But how to display change of values in the current open callout ? That is the think I do not get.
CustomAnnotation class:
class CustomAnnotation: NSObject, MKAnnotation {
@objc dynamic var title: String?
@objc dynamic var subtitle: String?
@objc dynamic var coordinate: CLLocationCoordinate2D
@objc dynamic var distance: CLLocationDistance
var poiColor: String?
var poiPhone: String?
init(title: String, subtitle: String, coordinate: CLLocationCoordinate2D, poiColor: String, poiPhone: String, distance: CLLocationDistance) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
self.poiColor = poiColor
self.poiPhone = poiPhone
self.distance = distance
super.init()
}
}
CustomAnnotationView class:
class CustomAnnotationView: MKMarkerAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .required
canShowCallout = true
detailCalloutAccessoryView = createCallOutWithDataFrom(customAnnotation: annotation as? CustomAnnotation)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
removeAnyObservers()
}
override var annotation: MKAnnotation? {
didSet {
removeAnyObservers()
if let customAnnotation = annotation as? CustomAnnotation {
updateAndAddObservers(for: customAnnotation)
}
}
}
private var subtitleObserver: NSKeyValueObservation?
private var distanceObserver: NSKeyValueObservation?
private let subtitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let distanceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
}
private extension CustomAnnotationView {
func updateAndAddObservers(for customAnnotation: CustomAnnotation) {
subtitleLabel.text = customAnnotation.subtitle
subtitleObserver = customAnnotation.observe(\.subtitle) { [weak self] customAnnotation, _ in
self?.subtitleLabel.text = customAnnotation.subtitle
}
let locationManager = CLLocationManager()
let theLatitude:CLLocationDegrees = (locationManager.location?.coordinate.latitude)!
let theLongitude:CLLocationDegrees = (locationManager.location?.coordinate.longitude)!
// Get pin location
let pointLocation = CLLocation(latitude: customAnnotation.coordinate.latitude, longitude: customAnnotation.coordinate.longitude)
//Get user location
let userLocation = CLLocation(latitude: theLatitude, longitude: theLongitude)
// Return distance en meters
let distanceFromUser = pointLocation.distance(from: userLocation)
customAnnotation.distance = distanceFromUser*100
distanceLabel.text = String(format: "%.03f", customAnnotation.distance)+" cm"
distanceObserver = customAnnotation.observe(\.distance) { [weak self] customAnnotation, _ in
self?.distanceLabel.text = "\(customAnnotation.distance) cm"
}
}
func removeAnyObservers() {
subtitleObserver = nil
distanceObserver = nil
}
func createCallOutWithDataFrom(customAnnotation: CustomAnnotation?) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = true
view.addSubview(subtitleLabel)
view.addSubview(distanceLabel)
NSLayoutConstraint.activate([
subtitleLabel.topAnchor.constraint(equalTo: view.topAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
subtitleLabel.bottomAnchor.constraint(equalTo: distanceLabel.topAnchor),
distanceLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
distanceLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
distanceLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
if let customAnnotation = customAnnotation {
updateAndAddObservers(for: customAnnotation)
}
return view
}
}
And to finish:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
let annotation = annotation as? CustomAnnotation
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "CustomAnnotation") as? CustomAnnotationView
if annotationView == nil {
annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: "CustomAnnotation")
annotationView?.canShowCallout = true
} else {
annotationView?.annotation = annotation
}
return annotationView
}
Thank you.
You would appear to have correctly configured the observers for the
subtitleanddistance. The problem is that a change in location is not triggering an update todistance. Thus, there is nothing triggering the KVO.You have an observer for
distance, which will trigger an update of the label. But you are not changingdistance. You should remove theCLLocationManagercode from that routine where you add the observers, and instead create a location manager (not within the annotation view, though) which uses itsdelegateto update all of the annotation distances, e.g.:Obviously, you would remove the
CLLocationManagercode fromupdateAndAddObservers.