Proper use of beginBackgroundTaskWithExpirationHandler
Asked Answered
H

5

110

I'm a bit confused about how and when to use beginBackgroundTaskWithExpirationHandler.

Apple shows in their examples to use it in applicationDidEnterBackground delegate, to get more time to complete some important task, usually a network transaction.

When looking on my app, it seems like most of my network stuff is important, and when one is started I would like to complete it if the user pressed the home button.

So is it accepted/good practice to wrap every network transaction (and I'm not talking about downloading big chunk of data, it mostly some short xml) with beginBackgroundTaskWithExpirationHandler to be on the safe side?

Heti answered 25/4, 2012 at 16:16 Comment(2)
Also see hereLiv
See this tooLiv
M
171

If you want your network transaction to continue in the background, then you'll need to wrap it in a background task. It's also very important that you call endBackgroundTask when you're finished - otherwise the app will be killed after its allotted time has expired.

Mine tend look something like this:

- (void) doUpdate 
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        [self beginBackgroundUpdateTask];

        NSURLResponse * response = nil;
        NSError  * error = nil;
        NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error];

        // Do something with the result

        [self endBackgroundUpdateTask];
    });
}
- (void) beginBackgroundUpdateTask
{
    self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [self endBackgroundUpdateTask];
    }];
}

- (void) endBackgroundUpdateTask
{
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
    self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}

I have a UIBackgroundTaskIdentifier property for each background task


Equivalent code in Swift

func doUpdate () {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {

        let taskID = beginBackgroundUpdateTask()

        var response: URLResponse?, error: NSError?, request: NSURLRequest?

        let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)

        // Do something with the result

        endBackgroundUpdateTask(taskID)

        })
}

func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
    return UIApplication.shared.beginBackgroundTask(expirationHandler: ({}))
}

func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
    UIApplication.shared.endBackgroundTask(taskID)
}
Monosaccharide answered 25/4, 2012 at 16:23 Comment(23)
Yes, I do... otherwise they stop in when the app enters the background.Monosaccharide
do we need to do anything in applicationDidEnterBackground?Cerebrum
Only if you want to use that as a point to start the network operation. If you just want an existing operation to complete, as per @Eyal's question, you don't need to do anything in applicationDidEnterBackgroundMonosaccharide
Thanks for this clear example! (Just changed beingBackgroundUpdateTask to beginBackgroundUpdateTask.)Deppy
@AshleyMills, Looking at the doco, it says Each call to this method must be balanced by a matching call to the endBackgroundTask: method. - That suggests to me that you should add a some code before the beginBackgroundUpdateTask to end the background task if it is not invalid (to stop two from being created but only one ended). I have tested this but did not find any errors reported (the doco says the system kills the application if it is not done). Any thoughts?Irairacund
Nice! If I understand correctly, you start a "thread" (through GCD dispatch queue) and tell it to stay in background even if app is removed from foreground (beginBackgroundTaskWithExpirationHandler:). Is that correct?Koppel
Are there any concurrency concerns when doing // Do something with the result? That is, should we save the data to some internal structure and use it only when endBackgroundUpdateTask is called? (AFAIK it is always called on the main thread?)Koppel
If you call doUpdate multiple times in a row without the work is done, you will overwrite self.backgroundUpdateTask so previous tasks can't be ended properly. You should either store the task identifier each time so you end it properly or use a counter in the begin/end methods.Necrose
Its great, just add starting brace { for doUpdate functionCarlton
Do I need to request permission to run in the background? (e.g. modify something on my plist)Selfcontained
@Necrose Can you please suggest me solution for overwriting task? I am not able to stop the task in endBackgroundUpdateTask.Banlieue
@BharatDodeja the simplest solution would be to just call endBackgroundUpdateTask in at the beginning of beginBackgroundUpdateTask to make sure any existing task is officially ended before starting a new one. That should work, I think.Riviera
clear explanation , i want to upload 500 mb videos to s3 server , can i use this for background upload ??? which means for a long running processThyratron
Can we have the UIBackgroundTaskIdentifier as a local variable ? Especially on a blocked based network method . Since block will keep a copy of the variable ?Beaker
You should begin the background task before dispatching to the background queue. From the documentation of applicationDidEnterBackground:: "Because it's likely any background tasks you start in applicationDidEnterBackground: will not run until after that method exits, you should request additional background execution time before starting those tasks.". See also the example here: developer.apple.com/library/ios/documentation/iPhone/Conceptual/…Paracasein
Does anyone have a working solution to the problem of calling the code multiple times concurrently?Pickax
Can you explain why you want your Swift/Objective-C versions to be different? What happens if you download takes too long? Shouldn't the expriationHandler should end the task?Liv
Expiration handler doesn't cancel HTTP request. So background task will be marked as ended but HTTP connection will remain alive and probably killed by OS. Expiration handler should cancel request and mark background operation as ended: developer.apple.com/documentation/uikit/uiapplication/… (read doc about handler parameter)Annadiana
You cannot separate the call to begin from the call to end and store the background task ID in a property like this. These calls, along with the ID, must be local, because otherwise you can't do a background task if another background task has already started.Sepoy
@ArielBogdziewicz for me the http connection is being killed as the expirationHandler gets called when the app’s remaining background time reaches 0, anything can I do to keep running that HTTP call until that HTTP call gets completes and then I end the background taskTask
@RajaSaad Yes, I've found somewhere else that background tasks shouldn't be used for URLConnection as it's handled in background automatically. However I haven't investigated it yet.Annadiana
@ArielBogdziewicz the reason why I came for beginBackgroundTask(expirationHandler:) is that sending my app to the background while an api call in progress even for a second was resulting in Network connection lost on iOS 13.0Task
Anybody could please tell - I am trying to update location in background after every 10 minutes. Is there any limitation in doing so. Will it continue to run in background continuously?Bum
M
24

The accepted answer is very helpful and should be fine in most cases, however two things bothered me about it:

  1. As a number of people have noted, storing the task identifier as a property means that it can be overwritten if the method is called multiple times, leading to a task that will never be gracefully ended until forced to end by the OS at the time expiration.

  2. This pattern requires a unique property for every call to beginBackgroundTaskWithExpirationHandler which seems cumbersome if you have a larger app with lots of network methods.

To solve these issues, I wrote a singleton that takes care of all the plumbing and tracks active tasks in a dictionary. No properties needed to keep track of task identifiers. Seems to work well. Usage is simplified to:

//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];

//do stuff

//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];

Optionally, if you want to provide a completion block that does something beyond ending the task (which is built in) you can call:

NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
    //do stuff
}];

Relevant source code available below (singleton stuff excluded for brevity). Comments/feedback welcome.

- (id)init
{
    self = [super init];
    if (self) {

        [self setTaskKeyCounter:0];
        [self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
        [self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];

    }
    return self;
}

- (NSUInteger)beginTask
{
    return [self beginTaskWithCompletionHandler:nil];
}

- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
    //read the counter and increment it
    NSUInteger taskKey;
    @synchronized(self) {

        taskKey = self.taskKeyCounter;
        self.taskKeyCounter++;

    }

    //tell the OS to start a task that should continue in the background if needed
    NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [self endTaskWithKey:taskKey];
    }];

    //add this task identifier to the active task dictionary
    [self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];

    //store the completion block (if any)
    if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];

    //return the dictionary key
    return taskKey;
}

- (void)endTaskWithKey:(NSUInteger)_key
{
    @synchronized(self.dictTaskCompletionBlocks) {

        //see if this task has a completion block
        CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
        if (completion) {

            //run the completion block and remove it from the completion block dictionary
            completion();
            [self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];

        }

    }

    @synchronized(self.dictTaskIdentifiers) {

        //see if this task has been ended yet
        NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
        if (taskId) {

            //end the task and remove it from the active task dictionary
            [[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
            [self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];

        }

    }
}
Mastin answered 27/12, 2014 at 5:20 Comment(5)
really like this solution. one question though: how/as what did you typedef CompletionBlock? Simply this: typedef void (^CompletionBlock)();Millard
You got it. typedef void (^CompletionBlock)(void);Mastin
@joel, thanks but where is the link of the source code for this implementation, i,e, BackGroundTaskManager ?Chelton
As noted above "singleton stuff excluded for brevity". [BackgroundTaskManager sharedTasks] returns a singleton. The guts of the singleton are provided above.Mastin
Upvoted for using a singleton. I really don't think they are as bad as people make out!Rhodia
D
21

Here is a Swift class that encapsulates running a background task:

class BackgroundTask {
    private let application: UIApplication
    private var identifier = UIBackgroundTaskInvalid

    init(application: UIApplication) {
        self.application = application
    }

    class func run(application: UIApplication, handler: (BackgroundTask) -> ()) {
        // NOTE: The handler must call end() when it is done

        let backgroundTask = BackgroundTask(application: application)
        backgroundTask.begin()
        handler(backgroundTask)
    }

    func begin() {
        self.identifier = application.beginBackgroundTaskWithExpirationHandler {
            self.end()
        }
    }

    func end() {
        if (identifier != UIBackgroundTaskInvalid) {
            application.endBackgroundTask(identifier)
        }

        identifier = UIBackgroundTaskInvalid
    }
}

The simplest way to use it:

BackgroundTask.run(application) { backgroundTask in
   // Do something
   backgroundTask.end()
}

If you need to wait for a delegate callback before you end, then use something like this:

class MyClass {
    backgroundTask: BackgroundTask?

    func doSomething() {
        backgroundTask = BackgroundTask(application)
        backgroundTask!.begin()
        // Do something that waits for callback
    }

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
Declivity answered 15/4, 2015 at 3:17 Comment(2)
The same problem like in accepted answer. Expiration handler doesn't cancel real task, but only marks it as ended. More over encapsulation causes that we are not able to do that ourselves. That's why Apple exposed this handler, so encapsulation is wrong here.Annadiana
@ArielBogdziewicz It's true that this answer provides no opportunity for additional cleanup in the begin method, but it is easy to see how to add that feature.Sepoy
S
9

As noted here and in answers to other SO questions, you do NOT want to use beginBackgroundTask only just when your app will go into the background; on the contrary, you should use a background task for any time-consuming operation whose completion you want to ensure even if the app does go into the background.

Therefore your code is likely to end up peppered with repetitions of the same boilerplate code for calling beginBackgroundTask and endBackgroundTask coherently. To prevent this repetition, it is certainly reasonable to want to package up the boilerplate into some single encapsulated entity.

I like some of the existing answers for doing that, but I think the best way is to use an Operation subclass:

  • You can enqueue the Operation onto any OperationQueue and manipulate that queue as you see fit. For example, you are free to cancel prematurely any existing operations on the queue.

  • If you have more than one thing to do, you can chain multiple background task Operations. Operations support dependencies.

  • The Operation Queue can (and should) be a background queue; thus, there is no need to worry about performing asynchronous code inside your task, because the Operation is the asynchronous code. (Indeed, it makes no sense to execute another level of asynchronous code inside an Operation, as the Operation would finish before that code could even start. If you needed to do that, you'd use another Operation.)

Here's a possible Operation subclass:

class BackgroundTaskOperation: Operation {
    var whatToDo : (() -> ())?
    var cleanup : (() -> ())?
    override func main() {
        guard !self.isCancelled else { return }
        guard let whatToDo = self.whatToDo else { return }
        var bti : UIBackgroundTaskIdentifier = .invalid
        bti = UIApplication.shared.beginBackgroundTask {
            self.cleanup?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        UIApplication.shared.endBackgroundTask(bti) // completion
    }
}

It should be obvious how to use this, but in case it isn't, imagine we have a global OperationQueue:

let backgroundTaskQueue : OperationQueue = {
    let q = OperationQueue()
    q.maxConcurrentOperationCount = 1
    return q
}()

So for a typical time-consuming batch of code we would say:

let task = BackgroundTaskOperation()
task.whatToDo = {
    // do something here
}
backgroundTaskQueue.addOperation(task)

If your time-consuming batch of code can be divided into stages, you might want to bow out early if your task is cancelled. In that case, just return prematurely from the closure. Note that your reference to the task from within the closure needs to be weak or you'll get a retain cycle. Here's an artificial illustration:

let task = BackgroundTaskOperation()
task.whatToDo = { [weak task] in
    guard let task = task else {return}
    for i in 1...10000 {
        guard !task.isCancelled else {return}
        for j in 1...150000 {
            let k = i*j
        }
    }
}
backgroundTaskQueue.addOperation(task)

In case you have cleanup to do in case the background task itself is cancelled prematurely, I've provided an optional cleanup handler property (not used in the preceding examples). Some other answers were criticised for not including that.

Sepoy answered 2/5, 2019 at 18:37 Comment(3)
I've now provided this as a github project: github.com/mattneub/BackgroundTaskOperationSepoy
Will the Queue continue (move on to the next operation) in the background? as the background handling is contained within the operation?Aminoplast
(above comment)Aminoplast
D
1

I implemented Joel's solution. Here is the complete code:

.h file:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;

@end

.m file:

#import "VMKBackgroundTaskManager.h"

@interface VMKBackgroundTaskManager()

@property NSUInteger taskKeyCounter;
@property NSMutableDictionary *dictTaskIdentifiers;
@property NSMutableDictionary *dictTaskCompletionBlocks;

@end


@implementation VMKBackgroundTaskManager

+ (id)sharedTasks {
    static VMKBackgroundTaskManager *sharedTasks = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedTasks = [[self alloc] init];
    });
    return sharedTasks;
}

- (id)init
{
    self = [super init];
    if (self) {

        [self setTaskKeyCounter:0];
        [self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
        [self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
    }
    return self;
}

- (NSUInteger)beginTask
{
    return [self beginTaskWithCompletionHandler:nil];
}

- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
    //read the counter and increment it
    NSUInteger taskKey;
    @synchronized(self) {

        taskKey = self.taskKeyCounter;
        self.taskKeyCounter++;

    }

    //tell the OS to start a task that should continue in the background if needed
    NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [self endTaskWithKey:taskKey];
    }];

    //add this task identifier to the active task dictionary
    [self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];

    //store the completion block (if any)
    if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];

    //return the dictionary key
    return taskKey;
}

- (void)endTaskWithKey:(NSUInteger)_key
{
    @synchronized(self.dictTaskCompletionBlocks) {

        //see if this task has a completion block
        CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
        if (completion) {

            //run the completion block and remove it from the completion block dictionary
            completion();
            [self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];

        }

    }

    @synchronized(self.dictTaskIdentifiers) {

        //see if this task has been ended yet
        NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
        if (taskId) {

            //end the task and remove it from the active task dictionary
            [[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
            [self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];

            NSLog(@"Task ended");
        }

    }
}

@end
Deplete answered 22/5, 2016 at 7:1 Comment(3)
Thanks for this. My objective-c isn't great. Could you add some code that show's how to use it?Pickax
can you please give a complete example on how to use ur codeJoceline
Very nice. Thanks.Longfaced

© 2022 - 2024 — McMap. All rights reserved.