How to run timer in background for unlimited time while iphone screen locked or unlocked and calling web service

Hi there


I've read many posts about running tasks in the background, however I have not been able to get an answer to our bluetooth entitled app's problem.


We have an app that connects to a bluetooth device(Beacon) and sends and receives BLE data in the background. There is no problem when the app is in foreground, the problem we face is when the app goes background and the device is locked. Yet it is extremely important that our app is sending data through web service every 5 minutes till the user exits the region


We start a task with beginBackgroundTask, set a timer to executes after 3 minutes. But when the user enters the region, beacon is detected and if the app is in the background, only one request is sent after 3 minutes and it doesn't repeat again till the user exits the region.


func doBackgroundTask() {

DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {

self.beginBackgroundTask()

self.countdownTimer = Timer.scheduledTimer(timeInterval: 180, target: self, selector: #selector(self.requestService), userInfo: nil, repeats: true)

RunLoop.current.add(self.countdownTimer, forMode: .common)

RunLoop.current.run()

}

}


func beginBackgroundTask() {

self.backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {

// you can't call endBackgroundTask here and you don't need to

})

assert(self.backgroundTask != UIBackgroundTaskIdentifier.invalid)

}


func endBackgroundTask() {

if self.backgroundTask != nil {

UIApplication.shared.endBackgroundTask(self.backgroundTask)

self.backgroundTask = UIBackgroundTaskIdentifier.invalid

}

}


How do I make the app run in the background for once in every 5 mins till the user exits the region.

Replies

How do I make the app run in the background for once in every 5 mins till the user exits the region.

There’s no direct way to do that. To start, I recommend you read my UIApplication Background Task Notes post, which explains a lot of the background (hey hey!) to this issue.

We start a task with

beginBackgroundTask
, set a timer to executes after 3 minutes. But when the user enters the region, beacon is detected and if the app is in the background, only one request is sent after 3 minutes and it doesn't repeat again till the user exits the region.

The problem here is that background tasks started while you’re in the background have a much shorter time limit. While the exact limit is not documented — it has changed in the past and it may well change again in the future — the current value is about 30 seconds.

So here’s what happens:

  1. The system resumes (or relaunches) your app in the background because the user enters your region.

  2. You schedule a timer for 3 minutes and start a background task to prevent the app from being suspended.

  3. After 30 seconds the background task expires. At this point one of two things happens:

    • If you have an expiry handler, that will be called. It must end the background task, at which point your app will suspend (A).

    • If you have no expiry handler, or it fails to end the background task promptly, the watchdog will kill your app (B).

  4. This means that the timer will not run after 3 minutes. Either your app is suspended (which will prevent the timer from running) or your app is terminated.

  5. This state continues until the user leaves your region. At that point one of two things happens:

    • In case A, the system will resume your app in the background. Your timer is long overdue, so it’ll fire now.

    • In case B, the system will relaunch your app in the background. As this is a new process, the timer no longer exists.

IMPORTANT If you want to test this, make sure to run your app from the Home screen rather than from Xcode. The Xcode debugger disables the watchdog, prevents your app from suspending, and so on. You can read more advice on this topic, albeit in a different context, in my Testing Background Session Code post.

Oh, btw, I wanted to address a few of your code snippets. To start, this is a concern:

DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
    self.beginBackgroundTask()
    self.countdownTimer = Timer.scheduledTimer(timeInterval: 180, target: self, selector: #selector(self.requestService), userInfo: nil, repeats: true)
    RunLoop.current.add(self.countdownTimer, forMode: .common)
    RunLoop.current.run()
}

Here you’re consuming a dispatch worker thread in order to run a timer. That’s very wasteful. Dispatch has a limited number of worker threads available to it, and you shouldn’t waste one like this.

If necessary you can use either:

  • Use the

    Thread
    API to start your own thread for this (see my Swift Forums post on this topic)
  • Use a Dispatch timer source, which target a queue

In this case, however, neither of these is a great idea. Consider the thread safety of your

backgroundTask
property. While it’s possible to start and end background tasks from any thread, the background task expiry handler will be called on the main thread. So, if the expiry handler touches the
backgroundTask
property, every reference to that property should be in the main thread [1].

The simplest way to achieve that is to schedule your timer on the main thread. If necessary, it can then dispatch work off to a secondary thread/queue.

And this one is also worrying:

self.backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
    // you can't call endBackgroundTask here and you don't need to
})

This is wrong. You must start every background task you end, even if the expiry handler is called. Failing to do this will get your app killed by the watchdog.

I’m not sure what caused you to say that “you can't call

endBackgroundTask
here”, but if you post some details about the problem you saw I should be able to explain what went wrong.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] The alternative is to confine every reference to the property to some other thread/queue, but then your expiry handler will need to call that thread/queue synchronously because your expiry handler must guarantee that the background task is ended before it returns.

Maybe you could prevent the app goes background and the device is locked.


UIApplication.shared.isIdleTimerDisabled = true

Hi Eskimo


I have read that from iOS 12 onwards the maximum background time allowed (for a normal app - not one with explicit background mode capabilities) appears to be 30 seconds. This is what I'm seeing in testing.


I'm implementing a clipboard timeout, and 30 seconds is a little on the short side.

I have read that from iOS 12 onwards the maximum background time allowed … appears to be 30 seconds.

That’s my understanding as well, although I’ve not tested it myself.

I'm implementing a clipboard timeout, and 30 seconds is a little on the short side.

If this change is causing you grief, you should file a bug explaining what you’re app does with its background task time and why the change is causing you problems.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Submitted but report FB6170898

Submitted but report FB6170898

Thanks for that.

Coming back to your specific use case, have you explored using the pasteboard’s expiration date support?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Ah ha. I knew about excluding pasteboard from handoff in ios 10 but missed the expiration timer. Just what I need. Many thanks!

What is self.backgroundTask here? Could you please provide code of self.backgroundTask?
Given this line:

Code Block
self.backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {


then self.backgroundTask must necessarily be declared as:

Code Block
var backgroundTask: UIBackgroundTaskIdentifier


Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"