NSURLSession background transfer : Callback for each video downloaded from a queue

1.4k views Asked by At

I am using background transfer service for downloading multiple videos using NSURLSession. Downloading is working fine when the App is in background mode and I am satisfied with it. My problem is, I want callback for each video downloaded from a queue.

I was expecting the following method to be called for each video downloaded:

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
 completionHandler:(void (^)())completionHandler

and following method when system has no more messages to send to our App after a background transfer:

-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session

But, both the methods are called when all downloads finish. I put 3 videos for downloading and then put App in background. Both methods called after all 3 videos were downloaded.


Here is what I am doing in those methods:

AppDelegate

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier 
 completionHandler:(void (^)())completionHandler
{    
    self.backgroundTransferCompletionHandler = completionHandler;
}

DownloadViewController

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];

    if (appDelegate.backgroundTransferCompletionHandler) 
    {
        void (^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
        appDelegate.backgroundTransferCompletionHandler = nil;
        completionHandler();
    }

    NSLog(@"All tasks are finished");
}

Is it possible to show user a local notification on downloading of each video ? Or, I will have to wait til all videos complete downloading in the background ?

If the answer is NO, then my question is what is the purpose of these two different callbacks ? What separates them from each other ?

2

There are 2 answers

12
Rob On BEST ANSWER

Is it possible to show user a local notification on downloading of each video ? Or, I will have to wait til all videos complete downloading in the background ?

The app is will only be restarted in background with handleEventsForBackgroundURLSession when all of the download associated with that session are done, not one-by-one. The idea of background sessions is to minimize the battery drain of keeping an running in the background (or repeatedly starting and then suspending), but rather to let the background daemon do that for you and let you know when everything is done.

Theoretically, you might be able to instantiate a separate background session with each, but this strikes me as an abuse of the background sessions (whose intent is to reduce how much time is spent spinning up your app and running it in background) and I wouldn't be surprised if Apple frowned upon that practice. It also would require a clumsier implementation (with multiple NSURLSession objects).

If the answer is NO, then my question is what is the purpose of these two different callbacks ? What separates them from each other ?

The purpose of the separate call backs is so that once your app is running again, it can do whatever post processing is needed for each of the downloads (e.g. moving them the files from their temporary location to their final location). You need separate callbacks per download, even if they're all called quickly in succession when the app is restarted in background mode. Plus, if the app happened to be running in foreground already, you could handle the individual downloads as they finish.


As an aside, LombaX is correct, that handleEventsForBackgroundURLSession should be starting up the background session. Personally, I make the completionHandler a property of my wrapper for the NSURLSession object, so handleEventsForBackgroundURLSession will instantiate it (getting it ready to call its delegate methods), and save the completionHandler there. It's the logical place to save the completion handler, you had to instantiate the NSURLSession and its delegate anyway, and it saves the URLSessionDidFinishEventsForBackgroundURLSession from needing to go back to the app delegate to get the saved completion handler.

Right or wrong, my typical implementation is to make the background NSURLSession object a singleton. Thus I end up with something like:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {    
    [BackgroundSession sharedSession].savedCompletionHandler = completionHandler;
}

That kills two birds with one stone, starting the background NSURLSession, and saving the completionHandler.

12
LombaX On

The problem here is that you are using NSURLSessionDelegate, which gives you the informations about the current download session. However, you want to know informations about the single tasks, not the entire session. For this reason, you should look at NSURLSessionTaskDelegate or NSURLSessionDownloadDelegate

Specifically, using NSURLSessionDownloadDelegate, you should implement this delegate method:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location

If the app is in background mode when the download is finished, this method won't be called automatically. However, a call to application:handleEventsForBackgroundURLSession:completionHandler: is made by the system, giving you the chance to rebuild your session and respond to the events (for example firing a notification, as you are asking for) More informations here:

In iOS, when a background transfer completes or requires credentials, if your app is no longer running, your app is automatically relaunched in the background, and the app’s UIApplicationDelegate is sent an application:handleEventsForBackgroundURLSession:completionHandler: message. This call contains the identifier of the session that caused your app to be launched. Your app should then store that completion handler before creating a background configuration object with the same identifier, and creating a session with that configuration. The newly created session is automatically reassociated with ongoing background activity.

Last, some years ago I made an open source project, a wrapper over NSURLSession. This project was made for iOS 7 so it could be using some deprecate methods, however the part covered by this answer is still valid. Link to FLDownloader

EDIT After Rob's answer, I did some check. It seems that the behavior is different between the app in suspended state and the app in killed state.

  • it seems that, when the app is closed (killed), the system will wake it up calling application:handleEventsForBackgroundURLSession:completionHandler: only when all download are finished. I tried attaching XCode to my iPhone and it seems correct.
  • However, at this link, in the "Background Transfer Considerations", it seems that if the app is in "suspended" state, it says:

If any task completed while your app was suspended, the delegate’s URLSession:downloadTask:didFinishDownloadingToURL: method is then called with the task and the URL for the newly downloaded file associated with it.

EDIT

The last assertion, event if confirmed by Apple docs, seems to be wrong. I checked personally with iPhone 6S, iOS 9.3.2, XCode and Instruments. I started two downloads and closed the app (suspended state, confirmed by Istruments activity monitor - the process was still alive but no cpu-time was consumed) but the URLSession:downloadTask:didFinishDownloadingToURL: method was not called. However, when both download were finished application:handleEventsForBackgroundURLSession:completionHandler: was called.