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 MKAnnotationView
s. 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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With