iosswiftmapkitmkpolyline

Drawing MKPolyline Which Follows User's Current Location


I am trying to draw a path behind the user as they move, tracking their path (like Strava or FitBit apps do when a user starts a workout). So far, the map centres on the user's location but does not start drawing when the user moves. I have tried to implement this with renderForOverlay, but it fails to do so when tested. The code is as follows:

ViewController.swift

import UIKit
import MapKit
import CoreLocation

class StartWorkoutViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {

    @IBOutlet weak var mapsView: MKMapView!
    @IBOutlet weak var startButton: UIButton!
    
    var locationManager: CLLocationManager!
    var allLocations: [CLLocation] = []
    
    @IBAction func startButton(_ sender: Any) {
        // Start the workout
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Request user's current location
        locationManager = CLLocationManager()
        locationManager?.requestAlwaysAuthorization()
        locationManager?.desiredAccuracy = kCLLocationAccuracyBest
        locationManager?.startUpdatingLocation()
        locationManager?.startUpdatingHeading()
        locationManager?.delegate = self
        
        mapsView?.showsUserLocation = true
        mapsView?.mapType = MKMapType(rawValue: 0)!
        mapsView?.userTrackingMode = .follow
        mapsView?.delegate = self
        
        let noLocation = CLLocationCoordinate2D()
        let viewRegion = MKCoordinateRegion(center: noLocation, latitudinalMeters: 100, longitudinalMeters: 100)
        mapsView?.setRegion(viewRegion, animated: true)
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // Add location to the array and prepare to draw a line between last location and current location
        print("Location Updated")
        allLocations.append(locations[0])
        
        let previousLocation = allLocations[allLocations.count - 1]
        let newLocation = locations[0]
        
        let previousCoordinates = previousLocation.coordinate
        let newCoordinates = newLocation.coordinate
        
        var area = [previousCoordinates, newCoordinates]
        let polyline = MKPolyline(coordinates: &area, count: area.count)
        mapsView.addOverlay(polyline)
    }
    
// DOES NOT WORK
    func mapView(_ mapsView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if overlay is MKPolyline {
            let polylineRenderer = MKPolylineRenderer(overlay: overlay)
            polylineRenderer.strokeColor = UIColor.red
            polylineRenderer.lineWidth = 4
            return polylineRenderer
        } else {
            return MKPolylineRenderer()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        // If the authorisation for the user's location has changed, ask again
        if status != .authorizedAlways {
            locationManager = CLLocationManager()
            locationManager?.requestAlwaysAuthorization()
        }
    }
}

Thank you!


Solution

  • The problem is that you’re grabbing a location, adding it to the array, and then creating a polyline from the last location in the array, allLocations[allLocations.count - 1], (which is now the current location) to the current location (i.e. to itself).

    So, grab the last item, previousCoordinate, from the array before you add the new location to it:

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let currentLocation = locations.first(where: { $0.horizontalAccuracy >= 0 }) else {
            return
        }
    
        let previousCoordinate = allLocations.last?.coordinate
        allLocations.append(currentLocation)
    
        if previousCoordinate == nil { return }
    
        var area = [previousCoordinate!, currentLocation.coordinate]
        let polyline = MKPolyline(coordinates: &area, count: area.count)
        mapsView.addOverlay(polyline)
    }
    

    I'd also suggest, as you see above, checking for the horizontal accuracy of the location update, to make sure it’s non-negative.

    Anyway, that yields:

    enter image description here


    A few other observations:

    1. I'd suggest retiring the noLocation pattern in viewDidLoad. My above pattern doesn't require that dummy value in the array.

    2. Another issue is that in didChangeAuthorization, you are instantiating a new CLLocationManager and not setting its properties. You are therefore losing the configuration of the original CLLocationManager in viewDidLoad. There’s no need to instantiate another one, but if you do, remember to configure it properly.