0

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.

1 Answers1

0

You would appear to have correctly configured the observers for the subtitle and distance. The problem is that a change in location is not triggering an update to distance. 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 changing distance. You should remove the CLLocationManager code from that routine where you add the observers, and instead create a location manager (not within the annotation view, though) which uses its delegate to update all of the annotation distances, e.g.:

class ViewController: UIViewController {
    @IBOutlet weak var mapView: MKMapView!

    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.distanceFilter = 5
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
}

extension ViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let currentLocation = locations.last(where: { $0.horizontalAccuracy >= 0 }) else { return }

        mapView.annotations
            .compactMap { $0 as? CustomAnnotation }
            .forEach {
                $0.distance = CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude)
                    .distance(from: currentLocation)
            }
    }
}

Obviously, you would remove the CLLocationManager code from updateAndAddObservers.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Rob, you just solved my problem and I'm thankful for that ! I just did not understand completely the KVO pattern before you explain crystal clear. So, it works :-) Thank you again. – Noway31 Aug 03 '21 at 02:28