Search code examples
swiftlocationmapkit

iOS MapKit Custom User location heading wrong


I am trying to make a custom "user location" pin, with heading rotation based on users location heading.

I used this answer: https://stackoverflow.com/a/58363556/894671 as a base and managed to get up and running a custom pin, that rotates based on the heading.

The problem:

While testing on a device, it seems, that the transformation using the provided heading is not correct. Only at 0/360 degrees it shows up correctly, but If I rotate around, I am seeing default MKMapKit shown heading to be correctly rotating, while my custom icon manages to rotate twice in that same time.

Please see the attached video: https://i.imgur.com/3PEm2MS.mp4

Demo uploaded here: https://github.com/GuntisTreulands/Demo123

But for all intents and purposes, here is AnnotationView:


class AnnotationView : MKAnnotationView, HeadingDelegate {
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

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

    func headingChanged(_ heading: CLLocationDirection) {
        // For simplicity the affine transform is done on the view itself
        UIView.animate(withDuration: 0.1, animations: { [unowned self] in
            self.transform = CGAffineTransform(rotationAngle: CGFloat(heading * .pi / 180.0 ))
        })
    }
}

and heading is forwarded from here:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
    {
        if let lastLocation = locations.last {
            userLocationAnnotation.coordinate = lastLocation.coordinate
        }
    }

I can't figure out, why my location heading is acting so weird.


Solution

  • Okay, I managed to kinda solve this.

    Here is the new result: https://i.sstatic.net/NHSvY.jpg

    What I did was:

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    
            if let heading = mapView.userLocation.heading {
                userLocationAnnotation.heading = -mapView.camera.heading + (heading.trueHeading > 0 ? heading.trueHeading : heading.magneticHeading)
            } else {
                userLocationAnnotation.heading = -mapView.camera.heading + (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
            }
        }
    
    

    So - in case I am using userlocation with trackingmode == .followWithHeading, this is working great. My icon is on top of the original icon.

    To hide original user icon and show yours instead - return a custom annotation view (with nothing to render) for userlocation:

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    
            guard !(annotation is MKUserLocation) else {
    
                var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "AnnotationView")
    
                if annotationView == nil {
                    annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "AnnotationView")
                }
    
                return annotationView
            }
    
            if let annotation = annotation as? Annotation {
                var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: NSStringFromClass(Annotation.self))
                if (annotationView == nil) {
                    annotationView = AnnotationView(annotation: annotation as MKAnnotation, reuseIdentifier: NSStringFromClass(Annotation.self))
                } else {
                    annotationView!.annotation = annotation as MKAnnotation
                }
    
                    annotation.headingDelegate = annotationView as? HeadingDelegate
                    annotationView!.image = UIImage.init(named: "user_pin")
    
                return annotationView
            }
            
            return nil
        }
    

    Thus the result is:

    1.) Get original apple maps user location + followWithHeading, but with a custom pin and correct location.

    2.) I can adjust my custom location pin location to some different coordinates, but keep in mind, that followWithHeading will rotate around the real userLocation coordinates.

    3.) I have also noticed, that without userlocation, the original calculation:

    userLocationAnnotation.heading = -mapView.camera.heading + (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
    

    Is also .. kinda okay, if I move and rotate. It is faulty, if I just rotate in place. Still cannot solve that one.

    I also managed to make a custom followWithHeading, with this function:

    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    
            mapView.centerCoordinate = userLocationAnnotation.coordinate
                
            mapView.camera.heading = (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
    
            userLocationAnnotation.heading = -mapView.camera.heading + (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
    
        }
    
    

    But the result is not as fluid as the original apple map rotation. If I rotate fast, then it is fine. But if I slowly turn my device, then the rotation is visibly not smooth.

    See the result: https://i.sstatic.net/RtkeG.jpg