2

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 ?

NSPratik
  • 4,714
  • 7
  • 51
  • 81

2 Answers2

1

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.

LombaX
  • 17,265
  • 5
  • 52
  • 77
  • Thanks LombaX, will it get called even when App is Backgrounded ? – NSPratik May 23 '16 at 09:38
  • I want to show local notification for each video downloaded, only if App is Backgrounded.. – NSPratik May 23 '16 at 09:39
  • 1
    AFAIR not automatically. If a download finishes when the app is in background, then the is relaunched calling [application:handleEventsForBackgroundURLSession:completionHandler](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/index.html#//apple_ref/occ/intfm/UIApplicationDelegate/application:handleEventsForBackgroundURLSession:completionHandler:), then you have to rebuild the session and register for the events – LombaX May 23 '16 at 09:43
  • How to do this: "rebuild the session and register for the events" ? And is it recommended to do so? – NSPratik May 23 '16 at 09:46
  • 1
    Yes, not only recomended, but this is the official method to restore the session from the background. When handleEvents... is called, the identifier for the session is passed. When you initialize the new session with that id,it will be associated with the background tasks. Look at the new informations and the link provided in the answer – LombaX May 23 '16 at 09:52
  • `handleEventsForBackgroundURLSession` will not get called until it downloads all the videos. How I would get a chance to create a new session with same identifier ? – NSPratik May 23 '16 at 10:15
  • 1
    `handleEventsForBackgroundURLSession` is called even if one of the download finishes and the other are still running. This method is called every time you have to handle an event for the background session, for example moving the file from the temp path (files are downloaded in temp path) to the final path when a download is finished. If in your case the method is not called, then you have to search for the problem elsewhere. Are you really sure about this? – LombaX May 23 '16 at 10:18
  • 1
    Probably it happens because you are not responding correctly to `handleEventsForBackgroundURLSession`. In that method, first of all you should recreate your session with the correct delegate to respond to event. Then, when you have finished to manage your callbacks, you have to call the completion handler with `completionHandler()`. Failing to do so, may result in the system to suppose that you are not ready to respond to callbacks, and then stopping to call you back (this is only an assumption) – LombaX May 23 '16 at 10:24
  • 1
    I'm checking. Regarding this, "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" probably something has changed in the last versions, I remember that previously the download were completed one-by-one. I'm doing some check on my apps – LombaX May 23 '16 at 10:39
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/112670/discussion-between-nspratik-and-lombax). – NSPratik May 23 '16 at 10:40
  • 1
    @LombaX - That quote from the Apple documentation is not incorrect. (Maybe poorly written, but not incorrect.) They're not saying anything about the precise timing of when the app will be started again, merely that when it is restarted, that those download completion delegate methods will then be called. That's all they're saying. – Rob May 23 '16 at 16:03
  • 2
    @Rob I can only agree with you... :-) – LombaX May 23 '16 at 16:19
1

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • In my code, `handleEventsForBackgroundURLSession` is not called on finishing download of each video. Is it ok ? Because LombaX telling that it should get called every time an video finishes downloading.. – NSPratik May 23 '16 at 10:33
  • Rob, can you please respond ? – NSPratik May 23 '16 at 10:57
  • 1
    Are you sure about this? "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.". I remember the opposite, however I used NSURLSession at this level some years ago and I could be wrong. About the other suggestion (use the background session as a singleton) it is the same approach I used in my demo project (Attached to my answer), although it that case I've forgotten to add the call to the completion handler – LombaX May 23 '16 at 11:02
  • Yes LombaX, I have made logs in both methods. They both get called after all download finish.. – NSPratik May 23 '16 at 11:04
  • 1
    I confirm the behavior explained by Rob. However it seems that the behavior is different when the app is killed (no process running) and when the app is suspended (process still alive). In the first case, iOS will wake up the app again only when all downloads associated to a session are finished (tested). In the second case, the session delegate is directly called (not yet tested), explained [here](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html). I updated my original post and do some tests – LombaX May 23 '16 at 11:25
  • 1
    After some tests, it seems that even the last assertion is false. Look at my original post. The Apple docs seems wrong about the explained behavior...or I'm misunderstanding something :-( – LombaX May 23 '16 at 11:50
  • 1
    @NSPratik - Yes, the app is only restarted only when all the downloads finish (or the user manually restarted app themselves). And I respectfully disagree with LombaX's assertion that the documentation in incorrect. – Rob May 23 '16 at 16:11
  • 1
    @Rob I said that the doc is incorrect OR that I'm misunderstanding something :-) this is not the first time I misunderstand Apple docs...sometimes it leaves to the reader too much interpretation – LombaX May 23 '16 at 16:23
  • Thanks guys for sharing thoughts. I am clear now. If I promote a local notification only when all download finish, it's absolutely fine. But, the discussion took me to a new way. Downloading doesn't work in my case, when an App is killed. What may be the reason ? – NSPratik May 24 '16 at 06:53
  • @NSPratik - Correct. If user kills the app manually (e.g. via double tap home button), that stops background requests. But if app is terminated through normal course of operation (e.g. user goes to another app and your app is suspended; even if user uses some other app that takes up so much memory that your app is eventually jettisoned due to memory pressure), then your background requests continue and your app will be restarted in background mode when the requests finish. – Rob May 24 '16 at 07:05
  • In short, that's not possible that downloading will continue if user removes App from the multitasking window - is it so ? I read Apple documentation and they have also used word "Killed" with "Suspended", that's why I was trying to do so.. – NSPratik May 24 '16 at 07:09
  • @NSPratik - It seems like you ask me to repeat myself every time I say anything. :) Yes, if user kills the app manually through double tap of home button, the background requests will be canceled and will not proceed. – Rob May 24 '16 at 07:14