iOS - Interactionless User Tracking

Using iOS’s wonderful Core Location libraries is easy enough but how can you track a user without launching the application?

I first stumbled upon this problem while researching a simple question: given my velocity and accelerometer data, could I teach a machine what “good” driving habits were vs. “bad” driving habits.

In order to accomplish this I wanted my application to be completely transparent. I had the following requirements for my application.

  1. The application should start automatically whenever it detected I had began driving

  2. It should automatically enter into a lower power mode whenever it detected I had stopped.

  3. It should be resillient to network failures in case I drove somewhere that my signal wasn’t that great.

  4. The application shouldn’t be a burden on the device or battery life.

Trial by Failure, Take 1

I began by researching how to accomplish efficient background networking requests which led me down a stomach turning rabbit hole of despair. iOS does something very well and that is to ensure that battery life is maximized, the user experience is always snappy, and kills / backgrounds any applications it deems unecessary at the time. The latter part was a huge problem for me. As soon as I would get accurate GPS coordinates in the background, iOS would put my application to sleep so that it would no longer save to Core Data nor would it transmit the data it saved over the network.

Semi-Solution 1: Significant Location Changes

Enter CLLocationManager().startMonitoringSignificantLocationChanges(). Introduced in iOS 4, this nifty little function does some pretty interesting things ( full docs for more information). This function allows a developer to observe for… well… significant location changes. One of the key take aways from this is directy from the documentation: If you start this service and your app is subsequently terminated, the system automatically relaunches the app into the background if a new event arrives.

So with this new information in hand, I simply created a singleton class to handle all my location updates, requesting user permission, and listening for and saving these location updates.

Trial by Failure, Take 2

Ecstatic that I finally found a solution to my problem of my app not staying awake long enough during my driving sessions, I hastily put together a basic UI and started driving around. Immediately I started noticing problem however. Yes, my app would awaken whenever I traveled a good bit of distance (generally more than 100 meters or so), but it would eventually go back to sleep and never wake back up.

It wasn’t until I discovered that in order for the app to really play nice with iOS, the following needs to happen.

1
2
3
4
5
6
private override init() {
// manager = CLLocationManager() setup as a class var
super.init()
manager.delegate = self
manager.allowsBackgroundLocationUpdates = true
}

Semi-Solution 2: Realtime Monitoring

So not only did the app need to wake up whenever significant distance threshold had been crossed, it also needed to actually start monitoring for “realtime” updates. To accomplish this, you can simply ask the location manager to monitor for location changes.

1
2
3
4
5
6
7
8
func realtimeUpdates() {
if !isMonitoringRealtime {
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
manager.distanceFilter = 0.01 // meters
manager.startUpdatingLocation()
isMonitoringRealtime = true
}
}

I setup the desired accuracy, distance filter and then start actually monitoring for location updates. However there is a huge problem with this setup battery drainage!!!!

Yet again, I was met with failure but I was getting closer. I thought to myself, how can I more efficiently track my location? I know that I really want realtime updates WHILE I’m driving but I DON’T want updates whenever I’m idling at a redlight.

Trial by Failure, Take 3

So finally I thought to myself, my primary goal is to analyze my driving behavior holistically and I would be willing to sacrifice loosing some data points if it meant achieving my 4 requirements. Since I decided acceleration data from a stand still (red light) was only a small portion of my driving habits, I used speed to determine what was idle and what was not. Arm with these semi-new requirements, I used a property on a CLLocation Object called speed.

1
2
3
Fair warning, Apple has some recommendations about using the speed property from a CLLocation object as 'informational' purposes only.

Personally, I found that it was fairly accurate for my purposes.

Final Solution

Overall I was able to achieve all my goals and requirements by combining both the significant location monitoring with real time monitoring. Using intelligent pauses, I stopped real time monitoring to save battery and waited for the vehicle to move again before monitoring again.

Code Snippet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//
// AppDelegate.swift

import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if launchOptions?.keys.contains(.location) ?? false {
NotificationManager.instance.realtimeLocationActivatedNotification()
LocationManager.instance.realtimeUpdates()
}
setupUI()
return true
}

func applicationDidEnterBackground(_ application: UIApplication) {
if (ConfigurationManager.instance.current()?.tracking ?? false) == true {
LocationManager.instance.monitorSignificant()
} else {
LocationManager.instance.stopMonitor()
}
}

func applicationWillTerminate(_ application: UIApplication) {
if (ConfigurationManager.instance.current()?.tracking ?? false) == true {
LocationManager.instance.monitorSignificant()
} else {
LocationManager.instance.stopMonitor()
}
}

private func setupUI() {
window = UIWindow(frame: UIScreen.main.bounds)
let vc = BaseViewController()
window?.rootViewController = vc
window?.makeKeyAndVisible()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//
// LocationManager.swift

import Foundation
import CoreLocation

protocol LocationManagerDelegate: class {
func permissionChanged(status: CLAuthorizationStatus)
func emittedCoordinate(location: CLLocation?)
}

class LocationManager: NSObject {

static let instance: LocationManager = LocationManager()

private let manager: CLLocationManager = CLLocationManager.init()

var interface: TruckInterface? = TruckInterface()

var configuration: ConfigurationManager = ConfigurationManager.instance

weak var delegate: LocationManagerDelegate?

private var isMonitoringRealtime: Bool = false {
didSet {
isMonitoringRealtime ? NotificationManager.instance.realtimeLocationActivatedNotification() : NotificationManager.instance.realtimeLocationDeactivatedNotification()
}
}

private override init() {
super.init()
manager.delegate = self
manager.allowsBackgroundLocationUpdates = true
}

private func save(location: CLLocation) {
// Save to Core Data
}

func requestAuthorization() {
manager.requestAlwaysAuthorization()
}

func monitorSignificant() {
manager.startMonitoringSignificantLocationChanges()
}

func realtimeUpdates() {
if !isMonitoringRealtime {
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
manager.distanceFilter = 0.01 // meters
manager.startUpdatingLocation()
isMonitoringRealtime = true
}
}

func stopRealtimeUpdates() {
isMonitoringRealtime = false
manager.stopUpdatingLocation()
}

func stopMonitor() {
manager.stopMonitoringSignificantLocationChanges()
}

func canTrack() -> Bool {
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways:
return true
default: return false
}
}
}

extension LocationManager: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedWhenInUse:
stopMonitor()
delegate?.permissionChanged(status: status)
case .authorizedAlways:
monitorSignificant()
delegate?.permissionChanged(status: status)
default: break
}
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// detect if we need to switch to realtime
locations.forEach { (location) in
delegate?.emittedCoordinate(location: location)
self.save(location: location)
print(location.speed)
if location.speed > 0.09 {
realtimeUpdates()
} else {
if isMonitoringRealtime {
stopRealtimeUpdates()
// go to sleep and wait for speed
monitorSignificant()
}
}
}
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}

If you enjoyed this article please share it with your friends!