Managing the user’s location

Managing the user’s location ūüß≠

In our map, we want to show the user’s location and be able to zoom into it by tapping the related Button we created in the last section. To do this, however, we must first be allowed to query the user’s location. This is only possible after the user confirms that he allows us access to his location information.¬†

To ask the user for his location, we need to add three more keys to the Custom iOS Target Properties. Open the project file by clicking on it in the Project Navigator and open the Info tab. Hover over the first entry of the Custom iOS Target Properties and add a new key by clicking on the “Plus”-icon.

The first key to add is “Privacy – Location Usage Description”. Enter a description of the purpose for which we need the user’s location as the value of this entry, e.g: “City Explorer needs access to your location information to show you photos that have been shot near to you.” Then we need two keys, “Privacy – Location When In Use Usage Description” and “Privacy – Location Always and When In Use Usage Description”. For these, you can reuse the value of the first entry.

Now we can query the user’s location and display it on the map after permission has been granted (the user will be asked for when we try to query the location for the first time).

To manage the location of the user, we create an ObservableObject. Create a new Swift file and name it “LocationManager.swift”. Make sure you import the MapKit framework again and create a “LocationManager” class that adapts the¬†ObservableObject¬†protocol.

import MapKit
import SwiftUI

class LocationManager: ObservableObject {
    
}

We need a so-called CLLocationManager property in our class. It does most of the work for us and queries the GPS data of the device the app runs on.

class LocationManager: ObservableObject {
    
    let userLocationManager = CLLocationManager()
    
}

When we initialize our LocationManager later on, the first thing we want to do is to check if the user has already been asked to provide his location. We find this out by checking the authorizationStatus property of our userLocationManager. If we haven’t already asked for the authorization to access the user’s location, we do so by writing:

class LocationManager: ObservableObject {
    
    let userLocationManager = CLLocationManager()
    
    func requestPermission() {
        switch userLocationManager.authorizationStatus {
        case .notDetermined:
            userLocationManager.requestAlwaysAuthorization()
        default:
            return
        }
    }
}

We want to call this function when launching our app. We can do this by adding a custom init function to our LocationManager

init() {
    requestPermission()
}

Next, we initialize the LocationManager as an @StateObject in our City_ExplorerApp struct. Then we inject it into the view hierarchy as an .environmentObject.

@main
struct City_ExplorerApp: App {
    
    @StateObject var locationManager = LocationManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(locationManager)
        }
    }
}

We can access this locationManager in our ContentView by adding the @EnvironmentObject property wrapper.

struct ContentView: View {
    
    @EnvironmentObject var locationManager: LocationManager
    
    var body: some View {
        //...
    }
}

We also equip our ContentView_Previews struct with its own LocationManager instance.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(LocationManager())
    }
}

Now, our app can access the current location of the user’s device (if the user gave us permission to do so). To make sure the map shows displays the user’s location, we go to our¬†MyMap¬†struct and define this in the¬†makeUIView¬†function:¬†

func makeUIView(context: Context) -> MKMapView {
    let map = MKMapView()
    map.showsUserLocation = true
    return map
}

If you run the app, our app will ask for permission to access the user’s location. If we grant it, the map shows us the current location. 

Note: In the simulator, you can simulate the user location by going to Features -> Location in the Xcode toolbar. You will probably have to zoom out of the map to see it. In the simulator, you can do this by simultaneously clicking the option key on your keyboard and dragging the mouse.

When we open our map, we want it to show either the user’s location or, if we don’t know it, a default location defined by us. To do this, we go back to our LocationManager and create a @Published variable called “currentRegion”. We assign an MKCoordinateRegion to it. An MKCoordinateRegion describes a geographic region centered around a specific latitude and longitude. As its initial value, you can choose the following coordinates with a radius of 10.000 meters (by the way, it’s Paris). 

@Published var currentRegion = MKCoordinateRegion(center: CLLocation(latitude: 48.864716, longitude: 2.349014).coordinate, latitudinalMeters: CLLocationDistance(10000), longitudinalMeters: CLLocationDistance(10000))


However, if we have access to the user’s location, we want to use it for our map’s initial region. For this purpose, we write in the init function of our LocationManager

init() {
    requestPermission()
    
    guard let userLocation = userLocationManager.location?.coordinate else {return}
    
    currentRegion = MKCoordinateRegion(center: userLocation, latitudinalMeters: CLLocationDistance(1000), longitudinalMeters: CLLocationDistance(1000))
}

The guard statement checks if we already know the location coordinates of the user. If that’s the case, we use them to define the center of our currentRegion. If not, the code below the guard statement will not be executed, and our default initial region remains the same. To let our MyMap zoom to the new currentRegion when it gets updated, we need a Binding to the locationManager of our ContentView

struct MyMap: UIViewRepresentable {
    
    @Binding var currentRegion: MKCoordinateRegion
    
    //...
    
}

Let’s initialize this Binding inside our ContentView.

MyMap(currentRegion: $locationManager.currentRegion)

Whenever the locationManager‘s published currentRegion variable gets updated, the Binding functionality causes the updateUIView function in our MyMap to be triggered. Currently, this function is still empty. Whenever it is called, we want the map to zoom to the assigned MKCoordinateRegion of the currentRegion. Therefore, we write:

func updateUIView(_ map: MKMapView, context: Context) {
    map.setRegion(currentRegion, animated: true)
}

To make sure that the map zooms to the currentRegion when initialized, we write in the makeUIView function of our MyMap:

func makeUIView(context: Context) -> MKMapView {
    //...
    map.setRegion(currentRegion, animated: true)
    return map
}

If we run the app again, we see that our map initially shows either the region around the user’s location or our default region. 

Finally, we would like to have the possibility to zoom to the device’s location when we tap on the corresponding button. Therefore, we add another function named “goToUserLocation” to our¬†LocationManager.

func goToUserLocation() {
        
}

Whenever the function gets called, we want to check if we have access to the location information. If this is the case, we update our currentRegion variable accordingly. For this purpose, we use a guard statement as we did inside the init function.

func goToUserLocation() {
    guard let userLocation = userLocationManager.location?.coordinate else {return}
    currentRegion = MKCoordinateRegion(center: userLocation, latitudinalMeters: CLLocationDistance(1000), longitudinalMeters: CLLocationDistance(1000))
}

We can now call this function from the Button of our ContentView.

Button(action: {
    locationManager.goToUserLocation()
}) {
    LocationButtonContent()
}

If we run the app again, move the map to somewhere and click on the location button. Here’s what happens: the goToUserLocation function of the locationManager gets called. The code inside it first checks if we have access to the location of the device. If so, we will use it to update the currentRegion property. Since this is a @Published variable, the Binding of the MyMap to the ObservedObject of the ContentView triggers the updateUIView function. This function uses the new MKCoordinateRegion to move the map to the user’s location.

Now that we display the user’s location on the map and the user can use the corresponding button to zoom to his location, we will now learn how the user can drop a pin at any location.

2 replies on “Managing the user’s location”

This access is deprecated with iOS 14
// let authorizationStatus = CLLocationManager().authorizationStatus

change code to:
func requestPermission() {
if userLocationManager.authorizationStatus == .notDetermined {
userLocationManager.requestAlwaysAuthorization()
}
}
init() {
requestPermission()
}

Leave a Reply

Your email address will not be published. Required fields are marked *