Map & User Location Tracking in SwiftUI

DevTechie Inc
Jun 24, 2022


Photo by Kelsey Knight on Unsplash

SwiftUI Map view has a parameter called showsUserLocation, but simply setting that to true doesn’t give you access to the user’s location. There is more work involved and today we will see how we can set that straight.

By the end of this article you will have a map with user location being displayed and tracked as shown in the screen capture below:

OK, so let’s get started.

First and foremost, a user’s location is a private affair and Apple is great at protecting it. Before tracking a user’s location, app devs are required to take permission from the user. Well, you all know that then why am I repeating it? 😵. There is a reason, Map view allows developers to set showsUserLocation property to true but doesn’t automatically start showing user’s location and that’s to protect user’s privacy and Apple wants app devs to be mindful when asking for location permission and also expects them to do some internal plumbing to show the location.

So in order to show a user’s location on a map, we will have to track their location, which is done via the CLLocationManager class. This class can seek user permission with various different intents and track their location with a wide range of configurations to provide accurate results. 
We will build a LocationManager which will provide us user’s location as it updates.

So let’s get started with a class called LocationManager. We will import the CoreLocation framework and make this class a final class. This class will also be a subclass of NSObject and will conform to the ObservableObject protocol so we can publish location information.

import Foundation
import CoreLocationfinal class LocationManager: NSObject, ObservableObject {
   
}
Next, we will create two properties, one to publish location information and other will be an instance of CLLocationManager

final class LocationManager: NSObject, ObservableObject {
    @Published var location: CLLocation?
 
    private let locationManager = CLLocationManager()
    
}
Whenever this class is initialized, we want to set some basic configuration for locationManager, i.e., desired accuracy for location tracking, any distance filtering option to control periodic location updates after significant distance updates.

We will also use this opportunity to request “WhenInUse” authorization from the user. Now setting this property along will not get us the permission, but we will also have to make a change in info. plist (which is now located in your project target). We need to add a new key in the Info list, so look for “Privacy — Location When In Use Usage Description” and set the description to something that will make sense to your user, because this description will be shown to user while permission is being asked.

Now back in initializer, we will also call method to start updating user’s location and make this class as delegate to receive those updates. Here is how our class looks like now:

final class LocationManager: NSObject, ObservableObject {
    @Published var location: CLLocation?
 
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = kCLDistanceFilterNone
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
        locationManager.delegate = self
    }
}
You should be getting an error like this at this point.

Let’s fix this, we will fix this. We will fix this by extending our class and make it a delegate for CLLocationManagerDelegate

extension LocationManager: CLLocationManagerDelegate {
   
}
This is where, we will provide a definition for a function which will receive updates for locations. We will receive location updates in this function. Location is received in an array, so we will take last item from this array and update our published location property on main thread.

extension LocationManager: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            DispatchQueue.main.async {
                self.location = location
            }
    }
}
We are done with LocationManager. Before we use our LocationManager, we will extend MKCoordinateRegion to add a couple of helper functions. One function to handle default region when location is not available, other function will return bendable value for region so we can use that inside the Map view.

extension MKCoordinateRegion {
    
    static func goldenGateRegion() -> MKCoordinateRegion {
        MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 37.819527098978355, longitude:  -122.47854602016669), latitudinalMeters: 5000, longitudinalMeters: 5000)
    }
    
    func getBinding() -> Binding<MKCoordinateRegion>? {
        return Binding<MKCoordinateRegion>(.constant(self))
    }
}
Now on to our SwiftUI Map view.

Let’s start with a StateObject property to initialize LocationManager instance.

struct MapWithUserLocation: View {
    
    @StateObject private var locationManager = LocationManager()
    
}
Next, we will create region computed property to get region from location manager. This is where we will use our MKCoordinateRegion extension functions.

struct MapWithUserLocation: View {
    
    @StateObject private var locationManager = LocationManager()
    
    var region: Binding<MKCoordinateRegion>? {
        guard let location = locationManager.location else {
            return MKCoordinateRegion.goldenGateRegion().getBinding()
        }
        
        let region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 500, longitudinalMeters: 500)
        
        return region.getBinding()
    }
    
}
Last but not the least the actual Map view to put it all together on screen:

struct MapWithUserLocation: View {
    
    @StateObject private var locationManager = LocationManager()
    
    var region: Binding<MKCoordinateRegion>? {
        guard let location = locationManager.location else {
            return MKCoordinateRegion.goldenGateRegion().getBinding()
        }
        
        let region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 500, longitudinalMeters: 500)
        
        return region.getBinding()
    }
    
    var body: some View {
        if let region = region {
            Map(coordinateRegion: region, interactionModes: .all, showsUserLocation: true, userTrackingMode: .constant(.follow))
                .ignoresSafeArea()
            
        }
    }
    
}
Final output will look like this:

Complete code below:

Location Manager:

import Foundation
import CoreLocationfinal class LocationManager: NSObject, ObservableObject {
    @Published var location: CLLocation?
 
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = kCLDistanceFilterNone
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
        locationManager.delegate = self
    }
}extension LocationManager: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            DispatchQueue.main.async {
                self.location = location
            }
    }
}
MKCoordinateRegion:

extension MKCoordinateRegion {
    
    static func goldenGateRegion() -> MKCoordinateRegion {
        MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 37.819527098978355, longitude:  -122.47854602016669), latitudinalMeters: 5000, longitudinalMeters: 5000)
    }
    
    func getBinding() -> Binding<MKCoordinateRegion>? {
        return Binding<MKCoordinateRegion>(.constant(self))
    }
}
MapWithUserLocation View:

import SwiftUI
import MapKitstruct MapWithUserLocation: View {
    
    @StateObject private var locationManager = LocationManager()
    
    var region: Binding<MKCoordinateRegion>? {
        guard let location = locationManager.location else {
            return MKCoordinateRegion.goldenGateRegion().getBinding()
        }
        
        let region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 500, longitudinalMeters: 500)
        
        return region.getBinding()
    }
    
    var body: some View {
        if let region = region {
            Map(coordinateRegion: region, interactionModes: .all, showsUserLocation: true, userTrackingMode: .constant(.follow))
                .ignoresSafeArea()
            
        }
    }
}struct MapWithUserLocation_Previews: PreviewProvider {
    static var previews: some View {
        MapWithUserLocation()
    }
}
With that we have reached the end of this article. Thank you once again for reading. Subscribe to our weekly newsletter at https://www.devtechie.com