Obtaining a user's location

The documentation for CoreLocation states the following as a one-line definition of what CoreLocation does.

Determine the current latitude and longitude of a device. Configure and schedule the delivery of location-related events.

In other words, figuring out a user's location is one of the two core features in the CoreLocation framework. For now, we will focus on determining the user's current latitude and longitude since that's the simplest and most basic thing we can do.

Before an app is allowed access to a user's location, it must ask permission to do so. Similar to how permission is asked for camera and motion access, you are required to specify in Info.plist that you want to query your user's location, and also the reason for doing so. The key that you must add to the Info.plist is NSLocationWhenInUseUsageDescription. The description should be short and concise, as usual, for example: Provides information about the nearby artwork. If you add this key and value pair, you are allowed to access the user's location while they're using the app. If you want to have access to the user's location while your app is in the background, you will need to add the Privacy - Location Always Usage Description key to your app.

Once you add the appropriate key to your plist file, you can use the CoreLocation framework in your app. Just as with the camera, adding the correct key to the plist file isn't enough. The user needs to manually allow your app to access their location. The steps to ask permission for the location are similar to the steps for the camera, but not quite the same.

When you ask permission to access the camera, a callback is used to communicate the result of this authorization to your application. For location services, a delegate method is called instead of a callback handler. This means that you must set a delegate on an instance of CLLocationManager that conforms to CLLocationManagerDelegate in order to be notified when the authorization status changes.

The CLLocationManager class acts as a gateway for the location services of the user's device. The location manager is not only used for location updates but also to determine heading, scheduling events, and more. We'll get to that later; for now, we will use the location manager to simply figure out the user's location.

To get started, import CoreLocation in ViewController.swift and make the ViewController class conform to CLLocationManagerDelegate by adding it to the class declaration. You should also add a property to ViewController that holds onto the location manager, as follows:

import CoreLocation 

class ViewController: UIViewController, CLLocationManagerDelegate{
let locationManager = CLLocationManager()

Next, add the following method to the class and make sure that you call it in viewDidLoad:

func setupLocationUpdates() { 
locationManager.delegate = self

let authStatus = CLLocationManager.authorizationStatus()
switch authStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
startLocationTracking()
default:
break
}
}

This method adds the view controller as the delegate for the location manager and verifies its authorization status. If the status is undetermined, we ask for permission, and if permission is already given, startLocationTracking is called immediately. This is a method we'll implement soon.

Whenever the permission status regarding the location changes, the location manager informs its delegate about this change. We must implement the following method to be notified of authorization changes:

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: 
CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
startLocationTracking()
}
}

In this method, we check the authorization status, and if the app is authorized to access the user's location, the startLocationTracking method is called. The implementation for this method is really short since we just want to tell the location manager to start sending us updates regarding the user's location. Before your app begins listening for location updates, you should make sure that location services are enabled, as shown in the following code snippet; you must do this because the user can disable location services entirely in the settings app:

func startLocationTracking() { 
if CLLocationManager.locationServicesEnabled() {
locationManager.startUpdatingLocation()
}
}

Now that the location manager has activated the GPS chip and started to process the user's location, you just need to implement a delegate method that is called whenever the location is updated. The method we should implement is locationManager(_:didUpdateLocations:).

The location manager supplies itself and an array of CLLocation instances to locationManager(_:didUpdateLocations:). The location array contains at least one CLLocation instance, but it could also contain multiple locations. This occurs if the location manager receives location updates faster than it can supply them to its delegate. All location updates are added in the order in which they occur. If you're just interested in the most recent location update, this means that you should use the last location in the array.

For our app, we will use the following implementation for locationManager(_:didUpdateLocations:):

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: 
[CLLocation]) {
guard let location = locations.last
else { return }

print(location)
manager.stopUpdatingLocation()
}

This implementation is perfect for ArtApp. We listen for location updates and pick the last one from the locations array. Then, we use the location, and after that, we stop listening for location updates. We do this for two reasons. The first is good manners; our app shouldn't listen for location updates longer than it has to. We just want to know where the user is at that moment in time, and we don't need continuous updates.

The second reason is to conserve battery life. If our app keeps listening for location updates all the time, the user's battery will empty faster than it needs to. As a developer, it's your job to make sure that your app does not needlessly drain the user's battery.

If you take a look at the printed output from the preceding method, you should see something similar to the following line:

<+52.30202835,+4.69597776> +/- 5.00m (speed 0.00 mps / course -1.00) @ 30-08-16 21:52:55 Midden-Europese zomertijd 

This output is a summary of some of the information that's available in the CLLocation class. Every instance has the GPS coordinates, the accuracy, the speed, the direction the user is traveling in, and a timestamp.

This is plenty of information for our app since the latitude and longitude are the only things needed to look up nearby pieces of art. If ArtApp was a real application, we could send the GPS coordinates to the backend, and it could respond with the number of available art pieces. However, we also want to have a readable location to display to the user, such as Amsterdam or New York.

The act of translating coordinates to a location or vice versa is called geocoding. To convert GPS coordinates to a human-friendly location, you will make use of the CLGeocoder class. This class has a single purpose: Geocoding locations. If you convert from coordinates to a human-readable location, you call the reverseGeocode(_:completionHandler:) method. If you already have an address and the coordinates for it, you call one of the address geocoding methods that are available.

If you're using the CLGeocoder class, it's important that your user has access to the internet, since CLGeocoder uses the network to geocode your requests. This is why you provide a callback to the geocoding methods; they don't return a value immediately. It's also important to be aware that geocoding is rate limited. Your app cannot make an infinite amount of geocoding requests, so it's important that you attempt to cache results locally to prevent unnecessary geocoding lookups.

A simple example of using the CLGeocoder is shown in the following code snippet:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:
[CLLocation]) {
guard let location = locations.last
else { return }

let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, completionHandler: {
placemarks, _ in
guard let placemark = placemarks?.first
else { return }

print(placemark)
})

locationManager.stopUpdatingLocation()
}

This example uses the last location received from the location manager and reverse geocodes it into a CLPlacemark instance. The completion handler for geocoding receives an array of possible CLPlacemarks that match the request, and an NSError. Both are optional, and only have a non-nil value if there is a real value available.

We'll get to displaying the address in a human-friendly form when we finish our login screen for ArtApp. Let's take a look at geofencing first.