5

I am almost towards the last phase of my app, which shows a live map of buses. So, basically, I have a timer which gets the latitude and longitude of a bus periodically from a xml sheet which provides real-time locations of the buses. I was able to setup the xml parser, animate the buses' movement and setup a custom (arrow) image for the buses.

However, the problem is, from an array of multiple buses, I can only get a single bus to rotate. Looking at the xml data, it's always the first bus from the xml sheet which is rotating. Earlier, I was having trouble with rotating even a single bus, so user "Good Doug" helped me out and I was able to get it working. You can see the post here: Custom annotation image rotates only at the beginning of the Program (Swift- iOS). I tried to use the same solution by making an array of MKAnnotationView for each bus. I'm not sure if this is the right approach. I'd be glad if someone could help me out with this :)

First of all, this is how the XML sheet looks like (In this example, there are two vehicles, so we need to track only two of them):

<body>
        <vehicle id="3815" routeTag="connector" dirTag="loop" lat="44.98068" lon="-93.18071" secsSinceReport="3" predictable="true" heading="335" speedKmHr="12" passengerCount="16"/>
        <vehicle id="3810" routeTag="connector" dirTag="loop" lat="44.97313" lon="-93.24041" secsSinceReport="3" predictable="true" heading="254" speedKmHr="62" passengerCount="1"/>
</body> 

Here's my implementation of a separate Bus class (in Bus.swift file). This could use some improvement.

class Bus : MKPointAnnotation, MKAnnotation  {
    var oldCoord : CLLocationCoordinate2D!
    var addedToMap = false

    init(coord: CLLocationCoordinate2D) {
        self.oldCoord = coord
    }
}

Here's the code from my ViewController.swift-

var busArray: [Bus!] = []           //Array to hold custom defined "Bus" types (from Bus.swift file)
var busViewArray : [MKAnnotationView?] = [nil, nil]                //Array to hold MKAnnotationView of each bus. We're assuming 2 buses are active in this case.
var vehicleCount = 0                // variable to hold the number of buses
var vehicleIndex = 0                // variable to check which bus the xml parser is currently on.
var trackingBusForTheVeryFirstTime = true

// My xml parser function:
func parser(parser: NSXMLParser!, didStartElement elementName: String!, namespaceURI: String!, qualifiedName qName: String!, attributes attributeDict: NSDictionary!) {
   if (elementName == "vehicle" ) {             
                let latitude = attributeDict["lat"]?.doubleValue                // Get latitude of current bus
                let longitude = attributeDict["lon"]?.doubleValue                // Get longitude of current bus
                let dir = attributeDict["heading"]?.doubleValue                        // Get direction of current bus

                var currentCoord = CLLocationCoordinate2DMake(latitude!, longitude!)                // Current coordinates of the bus

                // Checking the buses for the VERY FIRST TIME. This is usually the start of the program
                if (trackingBusForTheVeryFirstTime || vehicleCount == 0) {                  
                        let bus = Bus(coord: currentCoord)
                        self.busArray.append(bus)                        // Put current bus to the busArray
                        self.vehicleCount++                                        
                }
                else {        // UPDATE BUS Location. (Note: this is not the first time)

                        // If index exceeded count, that means number of buses changed, so we need to start over
                        if (self.vehicleIndex >= self.vehicleCount) {                         
                                self.trackingBusForTheVeryFirstTime = true                       
                                // Reset count and index for buses
                                self.vehicleCount = 0
                                self.vehicleIndex = 0
                                return
                        }

                        let oldCoord = busArray[vehicleIndex].oldCoord                   

                        if (oldCoord.latitude == latitude && oldCoord.longitude == longitude) {
                                // if oldCoordinates and current coordinates are the same, the bus hasn't moved. So do nothing.
                                return
                        }
                        else {                       
                                // Move and Rotate the bus:                       
                                UIView.animateWithDuration(0.5) {
                                        self.busArray[self.vehicleIndex].coordinate = CLLocationCoordinate2DMake(latitude!, longitude!)

                                        // if bus annotations have not been added to the map yet, add them:
                                        if (self.busArray[self.vehicleIndex].addedToMap == false) {
                                                self.map.addAnnotation(self.busArray[self.vehicleIndex])
                                                self.busArray[self.vehicleIndex].addedToMap = true
                                                return
                                        }

                                        if let pv = self.busViewArray[self.vehicleIndex] {
                                                pv.transform = CGAffineTransformRotate(self.map.transform, CGFloat(self.degreesToRadians(dir!)))         // Rotate bus
                                        }                          
                                }
                                if (vehicleIndex < vehicleCount - 1) 
                                        self.vehicleIndex++
                                }
                                else {
                                        self.vehicleIndex = 0
                                }
                                return     
                        }
                }
   }

Here's the viewForAnnotation that I implemented:

func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {

        let reuseId = "pin\(self.vehicleIndex)"
        busViewArray[self.vehicleIndex] = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId)

        if busViewArray[self.vehicleIndex] == nil {          
                self.busViewArray[self.vehicleIndex] = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)       
                busViewArray[vehicleIndex]!.image = imageWithImage(UIImage(named:"arrow.png")!, scaledToSize: CGSize(width: 21.0, height: 21.0))     
                self.view.addSubview(busViewArray[self.vehicleIndex]!)
        }
        else {
                busViewArray[self.vehicleIndex]!.annotation = annotation
        }  
        return busViewArray[self.vehicleIndex]
}

I am doubtful of my viewForAnnotation implementation. I am also unsure if it's okay have an array of MKAnnotationViews. Perhaps, my understanding of how annotation views work in iOS is wrong. I'd be glad if someone could help me out with this as I've been stuck on it for a while. Even if the overall implementation needs changing, I'd be glad to try it out. Here's a screenshot of the problem.

screenshot

Once again, please note that all the buses appear on the correct positions and move smoothly, but just one of them actually rotate. Thanks in advance.

Community
  • 1
  • 1
CocoaNuts
  • 179
  • 1
  • 13
  • 1
    If you set a breakpoint at "pv.transform = CGAffineTransformRotate(self.map.transform, CGFloat(self.degreesToRadians(dir!))) // Rotate bus" Does your code find a value for pv every iteration? – Moriya Nov 01 '15 at 03:13
  • Just checked. The code doesn't find a value for pv every iteration. – CocoaNuts Nov 01 '15 at 19:44
  • is it too hard for MapKit developers that add rotate or bearing func for annotationView? I thing making a whole map definitely were harder as they done ... – MHSaffari Dec 05 '18 at 13:34

1 Answers1

6

I don't think it's appropriate for the parsing code to manipulate annotation views directly. You don't know if they're visible, whether they've been instantiated yet, etc. The mapview is responsible for managing the annotation views, not you.

If you need to maintain cross reference between busses and annotations, do that, but don't maintain references to annotation views. Your app's interaction with the annotations should be limited to the annotations themselves. So create an annotation subclass that has a angle property.

class MyAnnotation : MKPointAnnotation {
    @objc dynamic var angle: CGFloat = 0.0
}

Then you can then have the annotation view subclass "observe" the custom annotation subclass, rotating as the annotation's angle changes. For example, in Swift 4:

class MyAnnotationView : MKAnnotationView {

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        addAngleObserver()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        addAngleObserver()
    }

    // Remember, since annotation views can be reused, if the annotation changes,
    // remove the old annotation's observer, if any, and add new one's.

    override var annotation: MKAnnotation? {
        willSet { token = nil        }
        didSet  { addAngleObserver() }
    }

    // add observer

    var token: NSKeyValueObservation!

    private func addAngleObserver() {
        if let annotation = annotation as? MyAnnotation {
            transform = CGAffineTransform(rotationAngle: annotation.angle)
            token = annotation.observe(\.angle) { [weak self] annotation, _ in
                UIView.animate(withDuration: 0.25) {
                    self?.transform = CGAffineTransform(rotationAngle: annotation.angle)
                }
            }
        }
    }
}

Or in Swift 3:

private var angleObserverContext = 0

class MyAnnotationView : MKAnnotationView {
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        addAngleObserver()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        addAngleObserver()
    }

    // add observer

    private func addAngleObserver() {
        if let annotation = annotation as? MyAnnotation {
            transform = CGAffineTransform(rotationAngle: annotation.angle)
            annotation.addObserver(self, forKeyPath: #keyPath(MyAnnotation.angle), options: [.new, .old], context: &angleObserverContext)
        }
    }

    // remove observer

    private func removeAngleObserver() {
        if let annotation = annotation as? MyAnnotation {
            annotation.removeObserver(self, forKeyPath: #keyPath(MyAnnotation.angle))
        }
    }

    // remember to remove observer when annotation view is deallocated

    deinit {
        removeAngleObserver()
    }

    // Remember, since annotation views can be reused, if the annotation changes,
    // remove the old annotation's observer, if any, and add new one's.

    override var annotation: MKAnnotation? {
        willSet { removeAngleObserver() }
        didSet  { addAngleObserver()    }
    }

    // Handle observation events for the annotation's `angle`, rotating as appropriate

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &angleObserverContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }

        UIView.animate(withDuration: 0.5) {
            if let angleNew = change![.newKey] as? CGFloat {
                self.transform = CGAffineTransform(rotationAngle: angleNew)
            }
        }
    }
}

Now, your app can maintain references to annotations that have been added to the map, and set their angle and this will be visually represented in the map view as appropriate.


And, a quick and dirty example of using this:

class ViewController: UIViewController {

    @IBOutlet weak var mapView: MKMapView!

    var annotation = MyAnnotation()

    private let reuseIdentifer = Bundle.main.bundleIdentifier! + ".annotation"

    private lazy var manager: CLLocationManager = {
        let manager = CLLocationManager()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        return manager
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView.register(MyAnnotationView.self, forAnnotationViewWithReuseIdentifier: reuseIdentifer)

        manager.requestWhenInUseAuthorization()
        manager.startUpdatingHeading()
        manager.startUpdatingLocation()

        mapView.addAnnotation(annotation)
    }

}

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        if annotation is MKUserLocation { return nil }

        return mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifer, for: annotation)
    }
}

extension ViewController: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last,
            location.horizontalAccuracy >= 0 else {
                return
        }
        annotation.coordinate = location.coordinate
    }

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        guard newHeading.headingAccuracy >= 0 else { return }
        annotation.angle = CGFloat(newHeading.trueHeading * .pi / 180)
    }
}

See previous revision of this answer for Swift 2 example.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • HI Rob, what you said makes a lot of sense. So, I tried setting the angle of each of the bus annotations like this: `busArray[vehicleIndex].angle = CGFloat(degreesToRadians!(dir!))`. However, the observer is not being invoked and the bus doesn't rotate. Is this the way to go, or would I have to do something else? – CocoaNuts Nov 01 '15 at 20:35
  • Start setting breakpoints inside your custom annotation subclass and your custom annotation view subclass and figure out where it's going wrong. For example, are you sure that `viewForAnnotation` is instantiating this correct subclass? Is it hitting the `addAngleObserver` line? You need to do some more debugging and find out where it's going awry. But if the observer isn't getting triggered, it's either because you aren't adding the observer, likely because either the view or its annotation is not of the right class or you neglected the `dynamic` qualifier or something like that. – Rob Nov 01 '15 at 22:25
  • But I used the above code to rotate multiple annotation views and it worked fine. – Rob Nov 01 '15 at 22:26
  • Thank you, good sir! Got it to work. Just changed some things in `viewForAnnotation` – CocoaNuts Nov 04 '15 at 19:08
  • @Rob could you show an example presenting the correct way to usage of it? – yerpy Mar 20 '17 at 09:46
  • You just add the `MyAnnotation` annotation to your map and then change the `angle` of this instance. – Rob Mar 20 '17 at 18:03
  • i wil user this code but now annotation show default image. following is code. viewFor annotation if !(annotation is MKPointAnnotation) { return nil } let annotationIdentifier = "pin" var aView = mapView.dequeueReusableAnnotationView(withIdentifier: annotationIdentifier) as? MyAnnotationView if aView == nil { aView = MyAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier) aView?.image = UIImage(named: "car") } else { aView?.annotation = annotation } – Nipul Daki Oct 05 '17 at 13:23
  • @NipulDaki - Your code looks fine. The problem is that my example above subclassed `MKPinAnnotationView`, and while that used to work, a few iOS versions ago, they changed it so that it was always a pin even if you set `image`. (Why it has an `image` property or doesn't present some warning/exception, I cannot say.) Anyway, subclass `MKAnnotationView` instead of `MKPinAnnotationView` and you should be OK. FYI, I updated answer for Swift 3 and 4, if you're interested. – Rob Oct 05 '17 at 16:38