Ios – significant location change does not trigger on device

cllocationmanagercore-locationios

I am using significant location change monitoring. When I run the code in the simulator and check Freeway option I get periodic location updates. I grab these updates and save them in NSUserDefaults and than update tableview every 10s, so I can see if I got any updates. If I run the app on a real device, I get zero updates. I kept the phone in my pocket and travelled over 80km, been in 2 cities. Zero updates. Not sure if I messed something up. I am attaching the code. The code is copy paste, feel free to test. Just make sure to use TableViewController in storyboard and set cell id to ID. What am I missing? I am testing on iphone 5.

info.plist:

enter image description here

edit: Found this in apple docs. Should my locationManager be created differently?

When an app is relaunched because of a location update, the launch
options dictionary passed to your
application:willFinishLaunchingWithOptions: or
application:didFinishLaunchingWithOptions: method contains the
UIApplicationLaunchOptionsLocationKey key. The presence of that key
signals that new location data is waiting to be delivered to your app.
To obtain that data, you must create a new CLLocationManager object
and restart the location services that you had running prior to your
app’s termination. When you restart those services, the location
manager delivers all pending location updates to its delegate.

edit2:

Based on this, the location should be updated at least every 15min. Bug in my code confirmed.

If GPS-level accuracy isn’t critical for your app and you don’t need
continuous tracking, you can use the significant-change location
service. It’s crucial that you use the significant-change location
service correctly, because it wakes the system and your app at least
every 15 minutes, even if no location changes have occurred, and it
runs continuously until you stop it.

edit3: added this code to AppDelegate didFinishLaunchingWithOptions: to see if app gets awaken. It does not get awaken-I see no 200 200 entry in table view. Something fishy is going on.

 if let options = launchOptions {
            print("options")
            if (launchOptions![UIApplicationLaunchOptionsLocationKey] != nil){
                locationManager.startUpdatingLocation()
                self.lat.append(Double(200))
                self.lon.append(Double(200))
                self.times.append(NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .NoStyle, timeStyle: .ShortStyle))
                NSUserDefaults.standardUserDefaults().setObject(lat, forKey: "lat")
                NSUserDefaults.standardUserDefaults().setObject(lon, forKey: "lon")
                NSUserDefaults.standardUserDefaults().setObject(times, forKey: "time")
            }

CODE:
//AppDelegate:

import UIKit
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {

    var lat:[CLLocationDegrees]!
    var lon:[CLLocationDegrees]!
    var times:[String]!
    var distances: [String]!

    var window: UIWindow?
    var locationManager: CLLocationManager!


    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        locationManager=CLLocationManager()
        locationManager.delegate=self
        locationManager.requestAlwaysAuthorization()

        let isFirstLaunch = NSUserDefaults.standardUserDefaults().objectForKey("lat")
        if isFirstLaunch == nil{
            lat = [CLLocationDegrees]()
            lon = [CLLocationDegrees]()
            times = [String]()
            NSUserDefaults.standardUserDefaults().setObject(lat, forKey: "lat")
            NSUserDefaults.standardUserDefaults().setObject(lon, forKey: "lon")
            NSUserDefaults.standardUserDefaults().setObject(times, forKey: "time")
        }else{
            lat = NSUserDefaults.standardUserDefaults().arrayForKey("lat") as! [CLLocationDegrees]
            lon = NSUserDefaults.standardUserDefaults().arrayForKey("lon") as! [CLLocationDegrees]
            times = NSUserDefaults.standardUserDefaults().objectForKey("time") as! [String]
//            distances = NSUserDefaults.standardUserDefaults().objectForKey("distance") as! [String]

        }  
        return true
    }



    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("location updated")

        self.lat.append(locations[0].coordinate.latitude)
        self.lon.append(locations[0].coordinate.longitude)
        self.times.append(NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .NoStyle, timeStyle: .ShortStyle))

        NSUserDefaults.standardUserDefaults().setObject(lat, forKey: "lat")
        NSUserDefaults.standardUserDefaults().setObject(lon, forKey: "lon")
        NSUserDefaults.standardUserDefaults().setObject(times, forKey: "time")

        print("Location: \(locations[0].coordinate.latitude)   \(locations[0].coordinate.longitude)")
    }

    func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
        print("did change AS")
        switch status {
        case .AuthorizedWhenInUse:
            locationManager.startMonitoringSignificantLocationChanges()
        case .AuthorizedAlways:
            print("to always")
            locationManager.startMonitoringSignificantLocationChanges()
            if lat.count==0{
                self.lat.append((locationManager.location?.coordinate.latitude)!)
                self.lon.append((locationManager.location?.coordinate.longitude)!)

                self.times.append(NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .NoStyle, timeStyle: .ShortStyle))

                NSUserDefaults.standardUserDefaults().setObject(lat, forKey: "lat")
                NSUserDefaults.standardUserDefaults().setObject(lon, forKey: "lon")
                NSUserDefaults.standardUserDefaults().setObject(times, forKey: "time")


            }

//            locationManager.startUpdatingLocation()
            break
        default:
            locationManager.stopMonitoringSignificantLocationChanges()
            break
        }
    }
}

// View Controller

import UIKit

class TableViewController: UITableViewController {

    let appDel = UIApplication.sharedApplication().delegate as! AppDelegate

    override func viewDidLoad() {
        super.viewDidLoad()

        NSTimer.scheduledTimerWithTimeInterval(10, target: self, selector: "updateTableView", userInfo: nil, repeats: true)
    }



    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return appDel.lon.count
    }


    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("ID", forIndexPath: indexPath)
        cell.textLabel?.text = "\(appDel.lat[indexPath.row])   \(appDel.lon[indexPath.row])    \(appDel.times[indexPath.row])"
        cell.textLabel?.font = UIFont.systemFontOfSize(9)


        return cell
    }

    func updateTableView(){
    self.tableView.reloadData()
    }
}

Best Answer

There are several problems with the code:

  1. The location updates are not started when the app is launched by iOS.
  2. Background location updates are not requested.
  3. The code relies on changes in authorization status to start location updates
  4. There is a missing break statement in the didChangeAuthorizationStatus method.

Here are the relevant documents:

The first document says a location manager object must be fully configured when iOS launches the app so the location event can be delivered to the app.

Upon relaunch, you must still configure a location manager object and call this method [startMonitoringSignificantLocationChanges] to continue receiving location events.

This is how I usually do it to ensure that the location manager is fully started regardless of if it was launched from the icon or by iOS (sorry I use ObjectiveC).

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

  NSLog(@"app launching");
  bgTask = UIBackgroundTaskInvalid;

  locationMgr = [[CLLocationManager alloc] init];
  [locationMgr setDelegate:self];
  if([locationMgr respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)])
    [locationMgr setAllowsBackgroundLocationUpdates:YES];
  CLAuthorizationStatus authorizationStatus= [CLLocationManager authorizationStatus];

  if([launchOptions valueForKey:UIApplicationLaunchOptionsLocationKey] != nil) {
    NSLog(@"relaunching because of significant location change - restarting SLC");
    [locationMgr startMonitoringSignificantLocationChanges];
  }
  else
  {
    if (authorizationStatus == kCLAuthorizationStatusAuthorizedAlways) {
        NSLog(@"launching with authorization to always use location - starting SLC");
        [locationMgr startMonitoringSignificantLocationChanges];
    }
    else
    {
        NSLog(@"launching with no authorization to always use location - requesting authorization");
        if([locationMgr respondsToSelector:@selector(requestAlwaysAuthorization)])
            [locationMgr requestAlwaysAuthorization];
    }
  }

  return YES;
}

Notice the call to setAllowsBackgroundLocationUpdates. This is new in iOS9 and must be done everytime the app starts if you want to receive locations updates in the background. (It is like a "I'm not kidding, I know I asked for them in the background modes, but I really do want them").

Apps that want to receive location updates when suspended must include the UIBackgroundModes key (with the location value) in their app’s Info.plist file and set the value of this property to YES.

Finally you can't rely on didChangeAuthorizationStatus being called. If you have the authorization kCLAuthorizationStatusAuthorizedAlways then calling [locationMgr requestAlwaysAuthorization] doesn't result in a change in authorization status, so didChangeAuthorizationStatus isn't called.

If the authorization status is already known when you call the requestWhenInUseAuthorization or requestAlwaysAuthorization method, the location manager does not report the current authorization status to this method.