Using Location and Maps in iOS app

Tracking the location of an iOS device involves Global Positioning System (GPS), cell ID location, and WiFi positioning service (WPS). By using three different services, Apple’s Core Location framework can pinpoint the location of an iOS device with varying degrees of accuracy.

Fortunately, Core Location hides the details of using these various technologies. Instead, Core Location lets you simply specify the degree of accuracy you wish, such as finding the location of an iOS device within 10 or 200 meters while also detecting any changes in the location of an iOS device. By tracking locations within a specified degree of accuracy and the distance an iOS device must travel before detecting movement, Core Location makes it easy for any app to identify the location of any iOS device.

The first step to using Core Location is to import the Core Location framework into an app like this:

import CoreLocation

After importing the Core Location framework, the next step is to access the location manager with any arbitrary name such as locationManager like this:

let locationManager = CLLocationManager()

A class needs to conform to the CLLocationManagerDelegate protocol, which you can do in one of two ways. First, you can simply add this to the class line like this:

class ViewController: UIViewController, CLLocationManagerDelegate {

Then you can declare that this class is the CLLocationManagerDelegate inside the viewDidLoad method:

override func viewDidLoad() { 
    super.viewDidLoad() 
    locationManager.delegate = self
}

The other way to conform to the CLLocationManagerDelegate protocol is to use an extension at the end of the class ViewController file like this:

extension ViewController: CLLocationManagerDelegate { }

Then you can declare that this class is the CLLocationManagerDelegate inside the viewDidLoad method:

override func viewDidLoad() {
    super.viewDidLoad()
    locationManager.delegate = self as? CLLocationManagerDelegate
}

When using Core Location, you need to define the amount of accuracy you want. Remember, the greater the accuracy, the more power the iOS device will require so it’s best to choose the level of accuracy your app absolutely needs. If you just need to identify the user’s geographical location such as a city, then you don’t need specific accuracy. However, if your app needs to know the iOS device’s precise location to locate the user such as for a ride-sharing service that needs to know where to pick up a passenger, then you’ll need greater precision.

You can define a specific level of accuracy in meters such as 150 meters. However, Core Location provides several constants you can use that define varying degrees of accuracy:

  • kCLLocationAccuracyBestForNavigation. The highest possible accuracy used for navigation apps
  • kCLLocationAccuracyNearestTenMeters. Accurate to within 10 meters
  • kCLLocationAccuracyHundredMeters. Accurate to within 100 meters
  • kCLLocationAccuracyKilometer. Accurate to the nearest kilometer
  • kCLLocationAccuracyThreeKilometers. Accurate to the nearest 3 kilometers

To define accuracy, you need to set the desiredAccuracy property to a value or to one of the preceding constants like this:

locationManager.desiredAccuracy = .kCLLocationAccuracyNearestTenMeters

In addition to defining the accuracy you want, you can also define a distance filter that specifies how far the iOS device needs to move to detect movement. The default value is stored in a constant called kCLDistanceFilterNone, which tells an app to be notified of all movement.

However, if you define a specific value in meters, you can modify this distance filter such as only detecting movement when an iOS device travels 100 meters such as

locationManager.distanceFilter = 100

Requesting a Location

Core Location gives you two ways to request the location of an iOS device. The first method requests the location once. This can be useful for apps that don’t need constant updating to track movement. To request location once, use the requestLocation method like this:

locationManager.requestLocation()

Because the requestLocation method only checks for a location once, it requires far less power than the second method, which requests locations continuously.

To track locations continuously, you need to use the startUpdatingLocation and stopUpdatingLocation methods like this:

locationManager.startUpdatingLocation()
locationManager.stopUpdatingLocation()

Core Location also offers two Boolean values you can modify as follows:

  • pausesLocationUpdatesAutomatically. Allows an app to temporarily pause updating a location
  • allowsBackgroundLocationUpdates. Defines whether an app can continue receiving location updates even when the app is suspended

Retrieving Location Data

When Core Location retrieves the location of an iOS device, it provides several different types of values:

  • coordinate.latitude and coordinate.longitude. Returns the latitude and longitude of a location.
  • horizontalAccuracy. Returns a distance of how accurate Core Location believes the defined location might be, measured in meters.
  • altitude. Returns the distance above or below sea level, measured in meters.
  • verticalAccuracy. Returns a distance of how accurate Core Location believes the altitude might be, measured in meters.
  • floor. Returns the floor of a building where the iOS device is located.
  • timestamp. Returns the time the location was retrieved.
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    if (error as NSError).code == CLError.locationUnknown.rawValue {
        return
    }
    print("didFailWithError \(error.localizedDescription)") 
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 
    let newLocation = locations.last! 
    print("didUpdateLocations \(newLocation)")
}

Some of the possible Core Location errors:

  • CLError.locationUnknown — the location is currently unknown, but Core Location will keep trying.
  • CLError.denied — the user denied the app permission to use location services.
  • CLError.network — there was a network-related error.

Requesting Authorization

Apps often need to request permission to access many hardware features of an iOS device. By forcing an app to request permission, Apple wants to make sure users authorize an app’s access to features such as the camera, the microphone, and the device’s location. Requesting authorization provides privacy for users and allows them to know exactly when an app might need to request access to specific hardware features.

Any app that uses Core Location must request authorization to track an iOS device’s location. Core Location provides two ways to request authorization:

  • requestWhenInUseAuthorization(). Uses location services only when your app is running.
  • requestAlwaysAuthorization(). Uses location services all the time.

In most cases, you’ll only want to use location services while your app is running. Besides using one of the preceding methods, an app also needs to modify its Info.plist file and add the Privacy – Location When In Use Usage Description key. In addition, you’ll need to add descriptive text explaining why your app needs to access location services.

let authStatus = CLLocationManager.authorizationStatus() 
if authStatus == .notDetermined {
    locationManager.requestWhenInUseAuthorization()
    return
}

This checks the current authorization status. If it is .notDetermined — meaning that this app has not asked for permission yet — then the app will request "When In Use" authorization. That allows the app to get location updates while it is open and the user is interacting with it.

if authStatus == .denied || authStatus == .restricted { 
    showLocationServicesDeniedAlert()
    return
}

Adding a Map

While you could display location data as text, you’ll more likely want to display a location visually on a map. To do this, you need to use a Map Kit View, which displays a map on the screen. Then you’ll need to import the MapKit framework such as

import MapKit

Finally, you’ll need to make the Map Kit View display the current location. To do this, you just need to set the showsUserLocation property to true such as

@IBOutlet var mapView: MKMapView! 
mapView.showsUserLocation = true

To see how to identify the location of an iOS device and display it on a map, follow these steps:

  1. Click the Main.storyboard file in the Navigator pane.
  2. Click the Library icon and drag and drop a Map Kit View at the top and a text view at the bottom of the view controller.
  3. Choose Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints at the bottom half of the submenu. Xcode adds constraints to the Map Kit View and the text view.
  4. Choose View > Assistant Editor > Show Assistant Editor, or click the Assistant Editor icon in the upper right corner of the Xcode window. Xcode shows the Main.storyboard side by side with the ViewController.swift file.
  5. Move the mouse pointer over the Map Kit View, hold down the Control key, and Ctrl-drag under the class ViewController line in the ViewController.swift file.
  6. Release the Control key and the left mouse button. A popup window appears.
  7. Click in the Name text field, type mapView, and click the Connect button. Xcode creates the following IBOutlet:
@IBOutlet var mapView: MKMapView!
  1. Move the mouse pointer over the text view, hold down the Control key, and Ctrl-drag under the class ViewController line in the ViewController.swift file.
  2. Release the Control key and the left mouse button. A popup window appears.
  3. Click in the Name text field, type myTextView, and click the Connect button. Xcode creates the following IBOutlet:
@IBOutlet var myTextView: UITextView!
  1. Choose View > Standard Editor > Show Standard Editor, or click the Standard Editor icon in the upper right corner of the Xcode window.
  2. Click the ViewController.swift file in the Navigator pane.
  3. Add the following underneath the import UIKit line:
import CoreLocation 
import MapKit

Add the following under the IBOutlets:

let locationManager = CLLocationManager()

Edit the class ViewController line as follows:

class ViewController: UIViewController, CLLocationManagerDelegate {

This makes the ViewController.swift file the CLLocationManagerDelegate. That means we need to define the ViewController.swift file as the delegate later.

Edit the viewDidLoad method as follows:

override func viewDidLoad() {
    super.viewDidLoad()
    locationManager.delegate = self
    locationManager.desiredAccuracy = .kCLLocationAccuracyNearestTenMeters 
    locationManager.requestWhenInUseAuthorization() 
    locationManager.startUpdatingLocation()
    mapView.showsUserLocation = true
}

This code makes the ViewController.swift file the CLLocationManager delegate. Then it defines the accuracy to 10 meters. The next line requests authorization to use location services, which means we’ll need to edit the Info.plist file later.

The startUpdatingLocation() method retrieves location data, while the showsUserLocation property is set to true to allow the Map Kit View to display the location. The entire ViewController. swift file should look like this:

import UIKit
import CoreLocation import MapKit
class ViewController: UIViewController, CLLocationManagerDelegate { 
    @IBOutlet var myTextView: UITextView!
    @IBOutlet var mapView: MKMapView!
    let locationManager = CLLocationManager()
    override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.delegate = self 
        locationManager.desiredAccuracy = .kCLLocationAccuracyNearestTenMeters 
        locationManager.requestWhenInUseAuthorization() 
        locationManager.startUpdatingLocation() 
        mapView.showsUserLocation = true
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let newLocation = locations.last {
            let latitudeString = "\(newLocation.coordinate.latitude)"
            let longitudeString = "\(newLocation.coordinate.longitude)" 
            myTextView.text = "Latitude: " + latitudeString + "\
                nLongitude: " + longitudeString
        } 
    }
}
  1. Click the Info.plist file in the Navigator pane.
  2. Move the mouse pointer over the bottom row until a + and – icon appears. Click the + icon to add another row.
  3. Click in the newly added row, and when a popup menu appears, choose Privacy – Location When In Use Usage Description.
  4. Click in the Value column of this row and type a message such as Need to access location services.
  5. Click the Run button or choose Product > Run. The Simulator screen appears.
  6. Choose Debug > Location > Apple to mimic the location of Apple’s headquarters. You can mimic a two-finger pinch gesture by holding down the Option key and dragging the mouse so you can zoom in and out of the displayed map.

Zooming in a Location

Although Core Location can find coordinates to our current location (or a simulated location such as Apple’s headquarters), the app currently displays the location on a large map. While the user could pinch to zoom in, ideally the app should display a closer view of our location automatically.

To do this, not only do we need to know a location, but we also need to define a region to show around that location. Defining a region around a location involves defining the following:

  • latitudeDelta. Measures north-to-south distance (measured in degrees) to display.
  • longitudeDelta. Measures east-to-west distance (measured in degrees) to display.

To see how to zoom in on a location, follow these steps:

Edit the class ViewController line as follows:

class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {

The MKMapViewDelegate gives us access to a mapView function that will let us zoom in to the defined location. After defining a MKMapViewDelegate, the next step is to make sure the map knows that the ViewController.swift file is the delegate.

Edit the viewDidLoad method by adding the mapView.delegate = self line at the end as follows:

override func viewDidLoad() {
    super.viewDidLoad()
    locationManager.desiredAccuracy = .kCLLocationAccuracyNearestTenMeters 
    locationManager.requestWhenInUseAuthorization() 
    locationManager.startUpdatingLocation() 
    mapView.showsUserLocation = true
    mapView.delegate = self
}

Add the following mapView function:

func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
    let zoomArea = MKCoordinateRegion(center: self.mapView.userLocation.coordinate, 
        span: MKCoordinateSpan (latitudeDelta: 0.05, longitudeDelta: 0.05)) 
    self.mapView.setRegion(zoomArea, animated: true)
}

Adding Annotations

An annotation allows the user to identify a location and place a cartoon pin on a map along with descriptive text. An annotation needs a location, which we can define by wherever the user presses on the map for an extended period of time, known as a long press.

Once we know where the user pressed on the map, we can display the annotation by adding it to the map along with any additional text. In addition, we’ll store the annotations in an array and include a button to clear the annotations from the map.

Add the following under the IBOutlets to define an array to hold all annotations added to the map:

var myAnnotations = [CLLocation]()

Edit the viewDidLoad method to recognize a long press gesture and add it to the map view as follows:

override func viewDidLoad() {
    super.viewDidLoad()

    locationManager.delegate = self 
    locationManager.desiredAccuracy = .kCLLocationAccuracyNearestTenMeters 
    locationManager.requestWhenInUseAuthorization() 
    locationManager.startUpdatingLocation() 
    mapView.showsUserLocation = true
    mapView.delegate = self
    let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(addPin(longGesture:))) 
    mapView.addGestureRecognizer(longGesture)
}

This long press gesture defines a function called addPin to respond to a long press, which means we now need to create that addPin function.

Add the following function under the viewDidLoad method:

@objc func addPin(longGesture: UIGestureRecognizer) { 
    let touchPoint = longGesture.location(in: mapView) 
    let touchLocation = mapView.convert(touchPoint, toCoordinateFrom: mapView)
    let location = CLLocation(latitude: touchLocation.latitude, longitude: touchLocation.longitude)
    let myAnnotation = MKPointAnnotation() 
    myAnnotation.coordinate = touchLocation 
    myAnnotation.title = "\(touchLocation.latitude) \ (touchLocation.longitude)" 
    myAnnotations.append(location) 
    self.mapView.addAnnotation(myAnnotation)
}

Click the Library icon and drag and drop a button on the user interface such as between the map view and the text view.

Double-click the button, type Clear Pins, and press Enter.

Edit this clearPins IBAction method as follows:

@IBAction func clearPins(_ sender: UIButton) { 
    mapView.removeAnnotations(mapView.annotations) 
    myAnnotations.removeAll()
}

Stopping location updates

If obtaining a location appears to be impossible for wherever the user currently is on the globe, then you need to tell the location manager to stop. To conserve battery power, the app should power down the iPhone’s radios as soon as it doesn’t need them anymore.

func startLocationManager() {
    if CLLocationManager.locationServicesEnabled() {
        locationManager.delegate = self 
        locationManager.desiredAccuracy = .kCLLocationAccuracyNearestTenMeters 
        locationManager.startUpdatingLocation()
        updatingLocation = true
    }
}

func stopLocationManager() {
    if updatingLocation {
        locationManager.stopUpdatingLocation() 
        locationManager.delegate = nil 
        updatingLocation = false
    } 
}

Reverse geocoding

Using a process known as reverse geocoding, you can turn a set of coordinates into a human-readable address. You’ll use the CLGeocoder object to turn the location data into a human-readable address and then display that address on screen.

let geocoder = CLGeocoder()

geocoder.reverseGeocodeLocation(newLocation, completionHandler: {
    placemarks, error in
        if let error = error {
            print("Reverse Geocoding error: \ (error.localizedDescription)")
            return
        }
        if let places = placemarks {
          print("Found places: \(places)")
        }
})