Getting started with Core Location

Introduction

Core Location is a very useful framework that Apple provides for us that is fairly easy to use in your own apps. Core Location allows you to program the device to determine the user's location. As you can imagine, this can be a privacy issue and Apple takes privacy very seriously so you will need to let the user know why you are tracking their location and they must accept it. You need to be thinking about this when you write your code because you can't expect every single one of your users to allow themselves to be tracked. This means you need to write your code to handle what happens when a user declines.

Getting Started

We are going to write a very simple app using SwiftUI that tells us our location with a button tap. Start a new SwiftUI project now. We will create a few properties to get us started. One property will be a bool to determine if our location is successfully retrieved or not. The second property will be an empty string that will hold our coordinates.

@State private var locationRetrieved = false
@State private var coordinates: String = ""

Then we can create a button that allows the user to tap to determine their location as well as a Text() that will display location if it's been retrieved. Go ahead and wrap this in a VStack.

VStack(spacing: 10) {
    Button("Show location") {
        // get location
    }
    
    if locationRetrieved {
        Text("Your coordinates are: \(coordinates)")
    }
}

When you're finished ContentView will look like this:

struct ContentView: View {
    @State private var locationRetrieved = false
    @State private var coordinates: String = ""
    
    var body: some View {
        VStack(spacing: 10) {
            Button("Show location") {
                // get location
            }
            
            if locationRetrieved {
                Text("Your coordinates are: \(coordinates)")
            }
        }
    }
}

Right now you can run your app and tap the button, but nothing interesting happens yet. Create a new Swift file called CoreLocationManager that will handle everything involving Core Location for us. To get started import the CoreLocation framework and make a CoreLocationManager class that inherits from NSObject and CLLocationManagerDelegate:

import CoreLocation

class CoreLocationManager: NSObject, CLLocationManagerDelegate {    

}

This class needs to hold a property of CLLocationManager() that will be used for nearly everything related to Core Location. Add the following as a class variable.

private var locationManager = CLLocationManager()

And now it's time for our first function. The first function we will write will set our locationManager delegate, set our desired accuracy, then allow the location manager to start determining the user's location. Add this function to your class.

func determineCurrentLocation() {
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestWhenInUseAuthorization()
    
    if CLLocationManager.locationServicesEnabled() {
        locationManager.startUpdatingLocation()
    }
}

For reference, your CoreLocationManager should look like this:

import CoreLocation

class CoreLocationManager: NSObject, CLLocationManagerDelegate {
    private var locationManager = CLLocationManager()
    
    func determineCurrentLocation() {
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        
        if CLLocationManager.locationServicesEnabled() {
            locationManager.startUpdatingLocation()
        }
    }
}

Perfect! Now we can go back to our ContentView and allow the button to determine our location. First, add a reference to our new class.

private let coreLocationManager = CoreLocationManager()

Then, replace // get location with coreLocationManager.determineCurrentLocation. Go ahead and run your app and tap the button to determine your location.

Uh oh, it didn't work? Remember how I said that Apple takes privacy seriously? This is one of the ways this happens. You haven't yet told the user why you want to access their location. You can even see a warning in the console that looks like this:

2021-04-02 18:16:42.421676-0600 How-To-Use-Core-Location[4182:132602] This app has attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an “NSLocationWhenInUseUsageDescription” key with a string value explaining to the user how the app uses this data

This is easily handled in the .plist file of your project. Add the following key-value.

Key: Privacy - Location When In Use Usage Description
Value: Please give us authorization to access your location so we can tell you your location

The value can say anything you want, but if it's not descriptive enough then Apple will reject your app from the app store.

Great, now you can run your app, tap the button, and see apple's built-in alert that will attempt to obtain confirmation from the user.

Next, you'll see that your Text isn't updated. The first reason is that we never toggled the boolean to allow it to show. The second reason is that we've only asked the app to update the location, but we never actually get that location. Let's head back over to CoreLocationManager and create a new function to return a coordinate. It's possible that we aren't able to get the user's location, so we need to make sure the return value is optional. Create the following function:

func getCurrentLocation() -> CLLocationCoordinate2D? {
    determineCurrentLocation()
    
    return locationManager.location?.coordinate
}

This will allow the device to use the previous function we created, then return the coordinate if it can be found. Head back over to ContentView.swift and edit what your button does to this:

determineLocation()

Now we need to create this function (still in ContentView.swift) which will do a few things. First, it will use our core location manager to get our current location. If we get a nil value, let's bail out and set our locationRetrieved variable to false. If we succeed let's break apart our longitude and latitude, update our coordinates text, then toggle our locationRetrieved variable to true.

func determineLocation() {
    guard let location = coreLocationManager.getCurrentLocation() else {
        locationRetrieved = false
        return
    }
    let latitude = location.latitude
    let longitude = location.longitude
    coordinates = "\(latitude), \(longitude)"
    locationRetrieved = true
}

Voila! You should now see your location appear in your app. Note that if you are using a simulator then you'll need to ensure there is a location enabled. This can be done by going to Features -> Location, and selecting one of the locations that is on the list. Simulators always default to none, so there's a high chance you'll need to do this.

Extending past coordinates

Core location is very powerful. It can do more than just determine your exact coordinates (which is a bit creepy), but it can even tell you the city, state, and address of where you. Let's create a new function inside CoreLocationManager.swift.

We are going to do the same thing as we did before by adding a bool, address string, new function to get address from ContentView.swift and a new function to get your address in CoreLocationManager.swift. Edit your ContentView to this:

struct ContentView: View {
    @State private var locationRetrieved = false
    @State private var addressRetrieved = false
    @State private var coordinates: String = ""
    @State private var address: String = ""
    private let coreLocationManager = CoreLocationManager()
    
    var body: some View {
        VStack(spacing: 10) {
            Button("Show location") {
                determineLocation()
            }
            
            if locationRetrieved {
                Text("Your coordinates are: \(coordinates)")
            }
            
            Button("Show address") {
                showAddress()
            }
            
            if addressRetrieved {
                Text("Your address is: \(address)")
            }
        }
    }
    
    func determineLocation() {
        guard let location = coreLocationManager.getCurrentLocation() else {
            locationRetrieved = false
            return
        }
        let latitude = location.latitude
        let longitude = location.longitude
        coordinates = "\(latitude), \(longitude)"
        locationRetrieved = true
    }
    
    func showAddress() {
        coreLocationManager.getAddress { returnedAddress in
            guard let unwrappedAddress = returnedAddress else {
                addressRetrieved = false
                return
            }

            address = unwrappedAddress
            addressRetrieved = true
            
        }
        
    }
    
}

You'll notice something different with our showAddress function. When we use Core Location to get our address it will run in the background and return our information in a closure. So we need a completion handler to handle this for us. Not too difficult if you're familiar with closures, but it's a bit strange if you're not used to it. We also need to write our coreLocationManager function to return an escaping completion handler, because it may take a while to get the information back. When I say it may take a while, I mean in computer terms. It will be near-instantaneous from your perspective.

Let's write our getAddress function now in CoreLocationManager.swift. We need to first get our user's coordinates (which we already made a function for), then create a CLLocation object out of it. Then we need to create an instance of CLGeocoder and use reverse geocoding in order to get obtain something called placemarks. We can get an error here, so we first need to check if an error exists. If it does, just return your completion as nil. If not, check if placemarks exist (they should, but it's always good to be safe and prevent app crashes). If placemarks exist, we need to make sure we actually have some. It is possible to get more than one, but in this case, we just want to take the first one. I use a lot of guard statements, but feel free to unwrap however it makes you happy. Each placemark has different values that mean different things that may not make sense to you if you've never done this. The street address is called name, the city is called locality, and the state is called administrative area. Unwrap all of these, append it into one string, then return that string as your completion.

func getAddress(completion: @escaping (String?) -> Void) {
    guard let coordinates = getCurrentLocation() else { return }
    let location = CLLocation(latitude: coordinates.latitude, longitude: coordinates.longitude)
    
    let geocoder = CLGeocoder()
    geocoder.reverseGeocodeLocation(location) { placemarks, error in
        if error != nil {
            print("Error: \(error)")
            completion(nil)
        }
        
        if placemarks != nil {
            guard let first = placemarks?.first else {
                completion(nil)
                return
            }
            
            guard let street = first.name else { return }
            guard let city = first.locality else { return }
            guard let state = first.administrativeArea else { return }
            
            completion("\(street) \(city), \(state)")
        }
    }
}

Give your app a go! You'd be surprised at how accurate it is.

I hope I helped you learn something new today with Swift and iOS development. It's great being able to implement something that is as powerful as Core Location. Try exploring other things that Core Location offers as we barely touched the surface. Or, use the basics and create your own weather app that can determine the weather near you. If you'd like to find the source code for this project you can do so here.