beginBackgroundTask expirationHandler never called - ios

I'm trying to complete a task in the background using UIApplication.shared.beginBackgroundTask but for some reason the expirationHandler is never called. The task I'm trying to complete is a video export from photo library but sometimes the export cannot be completed in time while the user is using the app in the foreground.
This is the code I'm using :
func applicationDidEnterBackground(_ application: UIApplication) {
if backgroundTask == .invalid && UploadQueue.instance.hasMoreWork() {
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "ExportQueue") {
NSLog("DriveLog - System is requesting end. Still more work to do ...")
self.endBackgroundTask()
}
print("Invalid? \(backgroundTask == .invalid)")
NSLog("DriveLog - Starting background task: %i", backgroundTask.rawValue)
}
}
func endBackgroundTask() {
NSLog("DriveLog - End called")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
I'm also calling :
(UIApplication.shared.delegate as! AppDelegate).endBackgroundTask()
during my task if I finish it earlier.
However I never see my expirationHandler being called in the log.
I have also tried putting beginBackgroundTask when starting the task in foreground but I get a warning message about task expiration while being in foreground.

You have not understood what the expiration handler is. It is called only if your time expires. Hence the name.
As soon as you call begin, start your task in the next line (not in the expiration handler). And when you are finished, call end.
You thus need to end the background task in two places: in the expiration handler, and outside it after actually performing your task.
It is very important to call end in both places, because if you fail to do so, the system will decide that you are a bad citizen and will never grant you any extra background time at all.
So, this is the diagram of the flow you need to construct:
Also note that this has nothing to do with UIBackgroundModes. That's a totally different mechanism.

matt's answer covers everything. I'm just going to try to give the same answer in different words because your edit suggests that matt's answer wasn't clear to you. (Read it again, though, it really does cover everything I'm going to say here, just in different words.)
You should not call beginBackgroundTask in applicationDidEnterBackground. You call it when you start whatever task you want time for. In your example that's going to be somewhere inside of UploadQueue. You don't call beginBackgroundTask when going into the background. You call it when you're starting a task that you would like to finish even if you go into the background.
Background tasks generally do not belong to the UIAppDelegate. They belong to the thing that creates the task (in your case: UploadQueue). You can create all the background tasks you want. They cost almost nothing. It's not just one "I want background" at the app level. Read matt's flow chart closely.
It's unclear from your question why you expect the expiration handler to be called. Do you expect your task to task to take so long that the OS forces you to stop it? That's what the expiration handler is for. If you've built your system correctly, it should rarely be called. Your task should end long before it's expired.
For full docs on how to do this, see Extending Your App's Background Execution Time. In particular note the caution:
Don’t wait until your app moves to the background to call the beginBackgroundTask(withName:expirationHandler:) method. Call the method before performing any long-running task.

Related

How do you extend an iOS app's background execution time when continuing an upload operation?

I'd like a user's upload operation that's started in the foreground to continue when they leave the app. Apple's article Extending Your App's Background Execution Time has the following code listing
func sendDataToServer( data : NSData ) {
// Perform the task on a background queue.
DispatchQueue.global().async {
// Request the task assertion and save the ID.
self.backgroundTaskID = UIApplication.shared.
beginBackgroundTask (withName: "Finish Network Tasks") {
// End the task if time expires.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskInvalid
}
// Send the data synchronously.
self.sendAppDataToServer( data: data)
// End the task assertion.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskInvalid
}
}
The call to self.sendAppDataToServer( data: data) is unclear. Is this where the upload operation would go, wrapped in Dispatch.global().sync { }?
You have stumbled across a less-than-stellar code sample in Apple’s documentation.
First, if you perform a synchronous network request, you definitely should dispatch it to a background queue. If you don't, you risk having the watchdog process kill your app. But you shouldn’t dispatch the network request synchronously to the global queue, but rather asynchronously, or else you just end up with the same problem, namely blocking the main thread.
That having been said, one really should never perform network requests synchronously. You should perform them asynchronously and end the background task in the completion handler.
In that example, they use NSData, which we don’t use anymore.
Also UIBackgroundTaskInvalid doesn’t exist anymore. It is now UIBackgroundTaskIdentifier.invalid.
In Apple’s defense, the point of this code sample is the background task, not the network code. They really didn’t want to get into the weeds of the implementation of this network code and were trying to keep it simple. That having been said, it really is a horrible and outdated code example.
See this answer for a better example of how one might use background tasks. Also, if the upload might take more than 30 seconds, we wouldn’t use the background task at all, but a proper (but more complicated) background URLSession. (See Downloading files in the background. Upload tasks follow the same basic pattern outlined there, though make sure to upload from a file, not a Data.)

iOS: Handling OpenGL code running on background threads during App Transition

I am working on an iOS application that, say on a button click, launches several threads, each executing a piece of Open GL code. These threads either have a different EAGLContext set on them, or if they use same EAGLContext, then they are synchronised (i.e. 2 threads don't set same EAGLContext in parallel).
Now suppose the app goes into background. As per Apple's documentation, we should stop all the OpenGL calls in applicationWillResignActive: callback so that by the time applicationDidEnterBackground: is called, no further GL calls are made.
I am using dispatch_queues to create background threads. For e.g.:
__block Byte* renderedData; // some memory already allocated
dispatch_sync(glProcessingQueue, ^{
[EAGLContext setCurrentContext:_eaglContext];
glViewPort(...)
glBindFramebuffer(...)
glClear(...)
glDrawArrays(...)
glReadPixels(...) // read in renderedData
}
use renderedData for something else
My question is - how to handle applicationWillResignActive: so that any such background GL calls can be not just stopped, but also be able to resume on applicationDidBecomeActive:? Should I wait for currently running blocks to finish before returning from applicationWillResignActive:? Or should I just suspend glProcessingQueue and return?
I have also read that similar is the case when app is interrupted in other ways, like displaying an alert, a phone call, etc.
I can have multiple such threads at any point of time, invoked by possibly multiple ViewControllers, so I am looking for some scalable solution or design pattern.
The way I see it you need to either pause a thread or kill it.
If you kill it you need to ensure all resources are released which means again calling openGL most likely. In this case it might actually be better to simply wait for the block to finish execution. This means the block must not take too long to finish which is impossible to guarantee and since you have multiple contexts and threads this may realistically present an issue.
So pausing seems better. I am not sure if there is a direct API to pause a thread but you can make it wait. Maybe a s system similar to this one can help.
The linked example seems to handle exactly what you would want; it already checks the current thread and locks that one. I guess you could pack that into some tool as a static method or a C function and wherever you are confident you can pause the thread you would simply do something like:
dispatch_sync(glProcessingQueue, ^{
[EAGLContext setCurrentContext:_eaglContext];
[ThreadManager pauseCurrentThreadIfNeeded];
glViewPort(...)
glBindFramebuffer(...)
[ThreadManager pauseCurrentThreadIfNeeded];
glClear(...)
glDrawArrays(...)
glReadPixels(...) // read in renderedData
[ThreadManager pauseCurrentThreadIfNeeded];
}
You might still have an issue with main thread if it is used. You might want to skip pause on that one otherwise your system may simply never wake up again (not sure though, try it).
So now you are look at interface of your ThreadManager to be something like:
+ (void)pause {
__threadsPaused = YES;
}
+ (void)resume {
__threadsPaused = NO;
}
+ (void)pauseCurrentThreadIfNeeded {
if(__threadsPaused) {
// TODO: insert code for locking until __threadsPaused becomes false
}
}
Let us know what you find out.

Alamofire request gets stuck when entering background?

I'm using Alamofire to make a call to a webservice which takes a pretty long time to load. If the app goes into the background I get stuck with my loader when I return to the app. I imagine it's because the call never returns anything to my completion handler. How can I address this problem?
You can use background fetching to solve this problem. It can be done in the following way in Swift 3:
var backgroundTask: UIBackgroundTaskIdentifier? // global variable
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "backgroundTask") {
// Cleanup code should be written here so that you won't see the loader
UIApplication.shared.endBackgroundTask(self.backgroundTask!)
self.backgroundTask = UIBackgroundTaskInvalid
}
Call your alamofire service after this line. In the completion handler, end the task using the below lines.
UIApplication.shared.endBackgroundTask(self.backgroundTask!)
self.backgroundTask = UIBackgroundTaskInvalid
Please note that the app has some background time (backgroundTimeRemaining property) remaining before it enters the inactive state. You have to get your task done before that time. The handler is called shortly before the remaining background time reaches zero. Also, each call to the method beginBackgroundTask(withName:){} must be balanced by a matching call to the endBackgroundTask: method.
To make the code given above work, you need to adjust settings in your app. Go to "Targets" and click on "Capabilities" to make the following changes
After that, go to your info.plist file, and open it as Source to add the following code.
I hope this helps you. If you need more detailed information, these links might help
https://developer.apple.com/reference/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio
https://developer.apple.com/reference/uikit/uiapplication/1622970-endbackgroundtask
The problem can be solved by adding:
application.beginBackgroundTask { }
into func applicationDidEnterBackground(_ application: UIApplication)
and
application.endBackgroundTask(.invalid)
into func applicationWillEnterForeground(_ application: UIApplication)

Force early call to background task expiration handler on iOS devices for testing

I am testing and debugging the expiration block in - beginBackgroundTaskWithExpirationHandler:.
Is there a way to force the Block call so that it happens quicker, instead of waiting for about 10 minutes each time I need to debug it?
I am not interested in debugging the actual code in the block, rather I am interested in the sequence of calls and the backtrace, etc.; that's why I need the callback itself to happen, but 10 minutes each time is too long!
Emulating the expiration of the background task time:
Start your background task on controller viewDidLoad and Schedule a Timer that prints out the backgroundTimeRemaining every 1 second.
Send your app to background.
Trigger the action of your background task when the backgroundTimeRemaining is less than X seconds.
X can be known by testing when the expiration handler is triggered. because the backgroundTimeRemaining is not guaranteed to be accurate.
Code:
UIApplication.shared.beginBackgroundTask(withName: "Submit Order Background Task") { [weak self] in
// Your logic to handle the expiration.
}
// To submit order 1 time!
var submitted = false
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
print("background task timer: \(UIApplication.shared.backgroundTimeRemaining)")
if UIApplication.shared.backgroundTimeRemaining < 2 && !submitted {
self.submitOrder()
submitted = true
}
}
If you are looking to force the system to call the expiration block, I don't think that can be done. However, I would suggest that you isolate your background task block, then call it using a NSTimer or
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
At least thats how I would do it.
Meaning, that you would call the code in your expiration handler directly using a NSTimer (implied or explicitly). So, if in your expiration handler you called a method that handled the expiration. Then, you would set a timer for that method to be called outside of the "background task" lifecycle, but you would be only simulating it.

SendSynchronousRequest triggers background expiration handler - IOS

this is how I start a background task when application goes background ,
void applicationDidEnterBackground:(UIApplication *)application
{
btId = UIBackgroundTaskInvalid;
UIApplication* cuiApplication = [UIApplication sharedApplication];
void (^backgroundTimeRemainingExtenderHandler)() = ^() {
NSTimeInterval timeRemaining1 = [cuiApplication backgroundTimeRemaining];
if(btId != UIBackgroundTaskInvalid){
[proximityEngine StopEngine];
[cuiApplication endBackgroundTask:btId];
btId = UIBackgroundTaskInvalid;
}
};
btId= [cuiApplication beginBackgroundTaskWithExpirationHandler:backgroundTimeRemainingExtenderHandler];
if(bgmanager != nil){
[bgmanager BeginBackgroundTaskMainLoop];
}
}
My problem is that when my background task calls :
NSURLConnection sendSynchronousRequest
The expiration block is being called even though there is more time remaning , how can I prevent this ?
Regards ,
James
EDIT :
After reading the answer below : I still have 596 seconds left when querying the amount of time left and yet still IOS calls the expiration block handler.
beginBackgroundTaskWithExpirationHandler: is the means by which apps request a little extra background time to do some tidying up as a result of going into the background. However iOS reserves the right to decide how much time it will offer you, if any at all, and to kill your process if you fail to end within the required amount of time.
You don't get to execute in the background indefinitely and you don't get to pick your own time limit. You can query what you've been allocated via backgroundTimeRemaining but that's pretty much the full extent.
Per the documentation the handler is called "shortly before the application’s remaining background time reaches 0". So you should expect backgroundTimeRemaining not quite to be zero.
That being said, if your URL connection hasn't yet completed then you're just meant to note somewhere that it didn't complete and deal with the error next time you come back from the background, usually by trying again. That's what your expiration handler should do, and it needs to do it fast.
The extra time allotted to your app is non-negotiable however.
In my particular case - which I do not quite sure why it behaved the way it behaved , I performed the task on a different thread than the beginbackgroundtask thread , after sendsync returned in that thread the backgroundtask was interrupted by the OS .
When calling sendsync in the beginbackground original thread , it does not occur.
Not sure if its something logical and I did something wrong or an OS bug.

Resources