Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS Swift - Reload location function with NSTimer for app background doesn't work

I've got a problem with location services. I can't set up a function that updates my location coordinates in the background by a NSTimer. Here is my code from appDelegate:

var locationManager = CLLocationManager()

func applicationDidEnterBackground(application: UIApplication) {

    self.locationManager.delegate = self
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest

    self.theTimer = NSTimer(fireDate: NSDate(), interval: 40, target: self, selector: "handleTimer", userInfo: nil, repeats: true)
    NSRunLoop.currentRunLoop().addTimer(self.theTimer, forMode: NSDefaultRunLoopMode)

}

    func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
    var locValue:CLLocationCoordinate2D = manager.location.coordinate
    println("dinBack = \(locValue.latitude) \(locValue.longitude)")
    self.locationManager.stopUpdatingLocation()
}

func handleTimer(){

    println("started")
    self.locationManager.startUpdatingLocation()

}

PS. - Of course that i've imported corelocation. - When I get back into the app, the console prints what should have printed in the background.

like image 349
Robert Constantinescu Avatar asked May 02 '15 01:05

Robert Constantinescu


People also ask

Does NSTimer run in background?

Problem is, NSTimer requires an active run loop which is not always readily available on background queues. The main thread has an active run loop but this defeats the purpose of having our timer run in the background so its a definite no go. So, to get a dedicated background-queue-friendly timer, we use GCD.

How do I change the background location in Swift?

Location Updates in Background ModeGo the Application target → Sign in & Capabilities → Capabilities button on the left → Choose Background Modes. Then, enable location updates.

How do I get a background location update every n minutes in my IOS application?

You can use an NSTimer to request a location every minute with this method now and don't have to work with startUpdatingLocation and stopUpdatingLocation methods.


1 Answers

You can not make an NSTimer work like this while your application is in the background. NSTimer's are not "real-time mechanisms". From the official documentation:

Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.

Emphasis mine.

The important take away from this is that while your application is in the background, any run loop that your timer would have been scheduled on is not actively running.

As soon as your app returns to the foreground, this run loop fires back up, sees that your timer is overdue, and sends the message to the selector.


With iOS 7 and forward, if you want to perform operations in the background, you can tell the OS that you want to perform "background fetches".

To set this up, we must first tell the OS how frequently we want to fetch data, so in didFinishLaunching..., add the following method:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
    return true
}

We can pass any time interval here (for example, if we only want to check once a day). The value we pass in is only defining a minimum amount of time that should pass between checks, however. There is no way to tell the OS a maximum amount of time between checks.

Now, we must implement the method that actually gets called when the OS gives us an opportunity to do background work:

func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // do background work
}

We can do whatever we want within this method. There are two catches, however.

  1. This method is called while our app is in the background. The OS limits us to (I believe) thirty seconds. After thirty seconds, our time is up.
  2. We must call the completionHandler() (or the OS will think we used all of our time).

The completionHandler that gets passed in takes an enum, UIBackgroundFetchResult. We should pass it either .Failed, .NewData, or .NoData, depending upon what our actual results were (this approach is typically used for checking a server for fresh data).

So, our method might look like this:

func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // do stuff
    if let _ = error {
        completionHandler(.Failed)
    } else if results.count > 0 {
        completionHandler(.NewData)
    } else {
        completionHandler(.NoData)
    }
}

Keep in mind, we have absolutely zero control over how frequently the OS will actually let us run this code in the background. The OS uses several metrics in order to optimize the user's experience.

I think if your app reports .Failed to the completion handler, the OS might give you a second chance soon, however if you're abusing .Failed, the OS could probably blacklist your application from using background fetches (and Apple could deny your app).

If your app isn't reporting .NewData, the OS will let your app do background work less often. I'm not saying this because I recommend that you just always report .NewData. You should definitely report accurately. The OS is very smart about scheduling work. If you're passing .NewData when there isn't new data, the OS will let your app work more often than it may need to, which will drain the user's battery quicker (and may lead to them uninstalling your app altogether).

There are other metrics involved in when your app gets to do background work however. The OS is very unlikely to let any app do background work while the user is actively using their device, and it is more likely to let apps do background work while the user is not using their device. Additionally, OS is more likely to do background work while it is on WiFi and while it is plugged into a charger of some sort.

The OS will also look at how regularly the user uses your app, or when they regularly use it. If the user uses your app every day at 6pm, and never at any other time, it's most likely that your app will always get an opportunity to do background work between 5:30pm and 6pm (just before the user will use the app) and never during any other part of the day. If the user very rarely uses your app, it may be days, weeks, or months between opportunities to work in the background.

like image 142
nhgrif Avatar answered Oct 17 '22 20:10

nhgrif