If your drivers are busy delivering packages to customers (i.e. you are doing something right), one of the most basic things you'd want to know is where your drivers are right now, without having to give them a call. This might be because you want them to do a last minute pickup, or accurately tell your customers when their package will arrive.

In this blog post, I'll walk you through a simple worked example (with sample code) of an iOS app that retrieves GPS coordinates on your phone, saves and updates your location in a database, and lets you view it on an online map on a separate website. Working knowledge of Swift and Javascript is assumed.

Part 1: Implementing Driver Live Tracking on iOS
Part 2: Building a Live Driver Tracking Backend
Part 3: Adding a Live Driver Tracking Map to your Website

How this code is organized

There are two main components to the app. Location tracking is handled in Classes/Managers/LocationManager.swift while the main screen displaying your location and web service that updates the database is in Classes/Viewcontrollers/MainViewController.swift. To get started, clone the gps_tracking_app project from my Github repository here and open TLDemo.xcodeproj in Xcode. All the code is available under a permissive MIT license here and you are free to use it in part or in whole for your own projects.

How live tracking on iOS works

There are two ways to track your location - foreground tracking and background tracking. Background tracking with the Standard Location Service is what we are using because it still tracks the driver's location even when the app is minimized (but not closed), which could happen if the driver needs to take a phone call or switch to the Google Maps app for turn by turn navigation. Tracking your location is as easy as initializing a CLLocationManager object and call its startUpdatingLocation() method.

LocationManager.swift

Using the singleton design pattern, LocationManager is initialized as a static global variable that can be accessed anywhere in the app with the line static let shared = LocationManager().

LocationManager is set with a distanceFilter (the minimum distance measured in meters a device must move horizontally before an update event is generated) parameter, set to 20 so we only deliver location updates when the user has spent time in one location and then moves on e.g. when we are more than 20 meters away from our previously recorded location. Updating the location this way reduces battery usage considerably.

override init() {
  super.init()
  locManager.allowsBackgroundLocationUpdates = true
  locManager.distanceFilter = 20;
  locManager.pausesLocationUpdatesAutomatically = false
  locManager.delegate = self
}

Most of the action in LocationManager.swift happens in the requestLocation() function.

func requestLocation() {
  if #available(iOS 14.0, *) {
    if (locManager.authorizationStatus == .notDetermined) {
      locManager.requestAlwaysAuthorization()
      locManager.startUpdatingLocation()
    } else if (locManager.authorizationStatus == .denied) {
      //
    } else {
      locManager.startUpdatingLocation()
    }
  } else {
    // Fallback on earlier versions
    if CLLocationManager.authorizationStatus() == .notDetermined {
      locManager.requestAlwaysAuthorization()
      locManager.startUpdatingLocation()

    } else if (CLLocationManager.authorizationStatus() == .denied) {
      //
    } else {
      locManager.startUpdatingLocation()
    }
  }
}

Let's break this down step by step to see what's going on.

if (locManager.authorizationStatus == .notDetermined) {
  locManager.requestAlwaysAuthorization()
  locManager.startUpdatingLocation()
}

This piece of code checks that you have given the app access to the phone's location data. If you have not, it will launch a pop up notification requesting access and immediately update your location.

else if (locManager.authorizationStatus == .denied) {
  //
}

If you have explicitly denied access, do nothing. In a production app you'd probably want to add a prompt to let the user open the settings app and choose the option to let the app always have access to your phone's location data. An easy way to do this is by using a method like UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString).

Otherwise (if authorization to use the phone's GPS is successful), start tracking the phone's location.

else {
  locManager.startUpdatingLocation()
}

Calling the startUpdatingLocation() method causes the location manager to obtain an initial location fix (which may take several seconds) and notify its delegate by calling its locationManager(_:didUpdateLocations:) method. In our case, the delegate is MapViewController.swift.

MapViewController.swift

In the didUpdateLocation() method of MapViewController.swift we see:

func didUpdateLocation(_ location: CLLocation ? ) {
  //Call api to the backend
  self.updateCurrentLocationToServer(location)

  // Update UI
  self.updateHeaaderView()
  if let currentLocation = location?.coordinate {
    currentLocationMarker?.position = currentLocation
    currentLocationMarker?.rotation = location?.course ?? 0
    self.focusToLocation(location: currentLocation)
  }
}

This piece of code updates the map with our current location (indicated with a marker .png image) and calls the updateCurrentLocationToServer() method which does two things.

func updateCurrentLocationToServer(_ location: CLLocation ? ) {
  if !Cache.shared.allowTrackingLocation {
    return
  }
  if let lastUpdateLocation = Cache.shared.lastUpdateLocation {
    let time = Date().timeIntervalSince1970 - lastUpdateLocation.time
    if (time < 20) {
      return
    }
  }

  guard
  let _currentLocation = location?.coordinate
  else {
    return
  }
  Cache.shared.lastUpdateLocation = (lat: _currentLocation.latitude, lng: _currentLocation.longitude, time: Date().timeIntervalSince1970)
  RealtimeDBAPI.shared.trackingLocation(location: _currentLocation) {
    (result) in
    //
  }
}
  1. It updates our Cache object (discussed below) with the line Cache.shared.lastUpdateLocation = (lat:_currentLocation.latitude,lng:_currentLocation.longitude,time:Date().timeIntervalSince1970)
  2. It calls an API to update our database with the new real time location. RealtimeDBAPI.shared.trackingLocation(location: _currentLocation) { (result) }. We discuss this in detail in Part 2: Building a Live Driver Tracking Backend.

Cache.swift

Like LocationManager.swift, Cache.swift is a globally accessible singleton. It is initialized with the line static let shared = Cache() when the app is launched. It's most important method is lastUpdateLocation() which saves the phone's last known location.

var lastUpdateLocation: (lat: Double, lng: Double, time: Double) ? {
  set {
    let location: [String: Any] = ["lat": newValue?.lat ?? 0, "lng": newValue?.lng ?? 0, "time": newValue?.time ?? 0]
    userDefaults.set(location, forKey: USER_DEFAULT_KEY.LAST_UPDATE_LOCATION)
  }
  get {
    if let location = userDefaults.object(forKey: USER_DEFAULT_KEY.LAST_UPDATE_LOCATION) as ? [String: Any],
      let lat = location["lat"] as ? Double,
        let lng = location["lng"] as ? Double,
          let time = location["time"] as ? Double {
            return (lat: lat, lng: lng, time: time)
          }
    return nil
  }
}

Implementation and Next Steps

We’ve outlined a simple way to include location tracking in your iOS app, but the work doesn’t stop here. As your delivery service gets more popular, you'll might want to add customer notifications with real time ETAs ("Your delivery driver is arriving in 5 minutes!") or even a tracking link showing the location of your driver. In app driver tracking is the basis for all of these features.

In my next post, I'll show you how to save your driver's real time location in the cloud and make it available for other services to consume as an API.

Next: Part 2: Building a Live Driver Tracking Backend