54

I'm a bit confuse of how to take advantage of the new iOS 7 NSURLSession background transfers features and AFNetworking (versions 2 and 3).

I saw the WWDC 705 - What’s New in Foundation Networking session, and they demonstrated background download that continues after the app terminated or even crashes.

This is done using the new API application:handleEventsForBackgroundURLSession:completionHandler: and the fact that the session's delegate will eventually get the callbacks and can complete its task.

So I'm wondering how to use it with AFNetworking (if possible) to continue downloading in background.

The problem is, AFNetworking conveniently uses block based API to do all the requests, but if the app terminated or crashes those block are also gone. So how can I complete the task?

Or maybe I'm missing something here...

Let me explain what I mean:

For example my app is a photo messaging app, lets say that I have a PhotoMessage object that represent one message and this object has properties like

  • state - describe the state of the photo download.
  • resourcePath - the path to the final downloaded photo file.

So when I get a new message from the server, I create a new PhotoMessage object, and start downloading its photo resource.

PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;

self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    NSURL *filePath = // some file url
    return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    if (!error) {
        // update the PhotoMessage Object
        newPhotoMsg.state = kStateDownloadFinished;
        newPhotoMsg.resourcePath = filePath;
    }
}];

[self.photoDownloadTask resume];   

As you can see, I use the completion block to update that PhotoMessage object according to the response I get.

How can I accomplish that with a background transfer? This completion block won't be called and as a result, I can't update the newPhotoMsg.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Mario
  • 2,431
  • 6
  • 27
  • 34

3 Answers3

78

A couple of thoughts:

  1. You have to make sure you do the necessary coding outlined in the Handling iOS Background Activity section of the URL Loading System Programming Guide says:

    If you are using NSURLSession in iOS, your app is automatically relaunched when a download completes. Your app’s application:handleEventsForBackgroundURLSession:completionHandler: app delegate method is responsible for recreating the appropriate session, storing a completion handler, and calling that handler when the session calls your session delegate’s URLSessionDidFinishEventsForBackgroundURLSession: method.

    That guide shows some examples of what you can do. Frankly, I think the code samples discussed in the latter part of the WWDC 2013 video What’s New in Foundation Networking are even more clear.

  2. The basic implementation of AFURLSessionManager will work in conjunction with background sessions if the app is merely suspended (you'll see your blocks called when the network tasks are done, assuming you've done the above). But as you guessed, any task-specific block parameters that are passed to the AFURLSessionManager method where you create the NSURLSessionTask for uploads and downloads are lost "if the app terminated or crashes."

    For background uploads, this is an annoyance (as your task-level informational progress and completion blocks you specified when creating the task will not get called). But if you employ the session-level renditions (e.g. setTaskDidCompleteBlock and setTaskDidSendBodyDataBlock), that will get called properly (assuming you always set these blocks when you re-instantiate the session manager).

    As it turns out, this issue of losing the blocks is actually more problematic for background downloads, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks, such as setDownloadTaskDidFinishDownloadingBlock).

  3. An alternative, you could stick with default (non-background) NSURLSession, but make sure your app requests a little time to finish the upload if the user leaves the app while the task is in progress. For example, before you create your NSURLSessionTask, you can create a UIBackgroundTaskIdentifier:

    UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
        // handle timeout gracefully if you can
    
        [[UIApplication sharedApplication] endBackgroundTask:taskId];
        taskId = UIBackgroundTaskInvalid;
    }];
    

    But make sure that the completion block of the network task correctly informs iOS that it is complete:

    if (taskId != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:taskId];
        taskId = UIBackgroundTaskInvalid;
    }
    

    This is not as powerful as a background NSURLSession (e.g., you have a limited amount of time available), but in some cases this can be useful.


Update:

I thought I'd add a practical example of how to do background downloads using AFNetworking.

  1. First define your background manager.

    //
    //  BackgroundSessionManager.h
    //
    //  Created by Robert Ryan on 10/11/14.
    //  Copyright (c) 2014 Robert Ryan. All rights reserved.
    //
    
    #import "AFHTTPSessionManager.h"
    
    @interface BackgroundSessionManager : AFHTTPSessionManager
    
    + (instancetype)sharedManager;
    
    @property (nonatomic, copy) void (^savedCompletionHandler)(void);
    
    @end
    

    and

    //
    //  BackgroundSessionManager.m
    //
    //  Created by Robert Ryan on 10/11/14.
    //  Copyright (c) 2014 Robert Ryan. All rights reserved.
    //
    
    #import "BackgroundSessionManager.h"
    
    static NSString * const kBackgroundSessionIdentifier = @"com.domain.backgroundsession";
    
    @implementation BackgroundSessionManager
    
    + (instancetype)sharedManager {
        static id sharedMyManager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedMyManager = [[self alloc] init];
        });
        return sharedMyManager;
    }
    
    - (instancetype)init {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
        self = [super initWithSessionConfiguration:configuration];
        if (self) {
            [self configureDownloadFinished];            // when download done, save file
            [self configureBackgroundSessionFinished];   // when entire background session done, call completion handler
            [self configureAuthentication];              // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
        }
        return self;
    }
    
    - (void)configureDownloadFinished {
        // just save the downloaded file to documents folder using filename from URL
    
        [self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
            if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
                NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
                if (statusCode != 200) {
                    // handle error here, e.g.
    
                    NSLog(@"%@ failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
                    return nil;
                }
            }
    
            NSString *filename      = [downloadTask.originalRequest.URL lastPathComponent];
            NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
            NSString *path          = [documentsPath stringByAppendingPathComponent:filename];
            return [NSURL fileURLWithPath:path];
        }];
    
        [self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
            if (error) {
                // handle error here, e.g.,
    
                NSLog(@"%@: %@", [task.originalRequest.URL lastPathComponent], error);
            }
        }];
    }
    
    - (void)configureBackgroundSessionFinished {
        typeof(self) __weak weakSelf = self;
    
        [self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
            if (weakSelf.savedCompletionHandler) {
                weakSelf.savedCompletionHandler();
                weakSelf.savedCompletionHandler = nil;
            }
        }];
    }
    
    - (void)configureAuthentication {
        NSURLCredential *myCredential = [NSURLCredential credentialWithUser:@"userid" password:@"password" persistence:NSURLCredentialPersistenceForSession];
    
        [self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
            if (challenge.previousFailureCount == 0) {
                *credential = myCredential;
                return NSURLSessionAuthChallengeUseCredential;
            } else {
                return NSURLSessionAuthChallengePerformDefaultHandling;
            }
        }];
    }
    
    @end
    
  2. Make sure app delegate saves completion handler (instantiating the background session as necessary):

    - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
        NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], @"Identifiers didn't match");
        [BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
    }
    
  3. Then start your downloads:

    for (NSString *filename in filenames) {
        NSURL *url = [baseURL URLByAppendingPathComponent:filename];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        [[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
    }
    

    Note, I don't supply any of those task related blocks, because those aren't reliable with background sessions. (Background downloads proceed even after the app is terminated and these blocks have long disappeared.) One must rely upon the session-level, easily recreated setDownloadTaskDidFinishDownloadingBlock only.

Clearly this is a simple example (only one background session object; just saving files to the docs folder using last component of URL as the filename; etc.), but hopefully it illustrates the pattern.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I do understand how to do the same using the basic `NSURLSession` APIs in iOS 7/8. Can you maybe give an example of how to implement `setDownloadTaskDidFinishDownloadingBlock:` API? – p0lAris Oct 11 '14 at 10:10
  • @p0lAris See example at the end of my revised answer. – Rob Oct 11 '14 at 17:21
  • Thanks Rob. I have one very small follow up question. I have done the above (albeit in a different way). I have my core data model set up such that the images are binary data (as they very small images). And I want to be able to say — `someManagedObject.image = [NSData dataWithContentsOfFile:...]` after the download is done; where would be the appropriate play to implement that? – p0lAris Oct 12 '14 at 01:02
  • You could do that inside your `setDownloadTaskDidFinishDownloadingBlock`, too. Or perhaps you'd have that that block post a notification that whatever controller is handling the managed object context would observe, and then do it there, in that observer. – Rob Oct 12 '14 at 02:28
  • Oh wow. So are you trying to say when the OS opens the app in the background, it basically creates other objects as well? For example objects that could be handling my core data storage and I could simply talk to them? NICE. Thanks a lot. – p0lAris Oct 12 '14 at 04:32
  • hey great post, thanks. One quesiton is there a way to track progress after app was terminated? – Michal Gumny Nov 09 '14 at 13:32
  • 1
    @MichalGumny Obviously, not while the app is terminated. lol. But if user happens to restart the app while downloads are in progress, as long as you reinstantiate a background `NSURLSession` with the same identifier (i.e. don't rely upon `handleEventsForBackgroundURLSession` to reinstantiate the background session, like we usually do), then the `didWriteData` delegate method will be called as data continues to come in. – Rob Nov 09 '14 at 15:17
  • ofc I don't need to track progress while app is terminated :) thx for help – Michal Gumny Nov 09 '14 at 16:49
  • Does anyone know whether these rules apply when using Background Fetch. In my experiments the standard blocked based AFURLSessionManager works without an issue. Also it seems to me that's exactly how Background Fetch is supposed to work - a task with approx. 30 seconds to retrieve some data etc. and that either completes in time or not. Any thoughts? – wuf810 Feb 12 '15 at 08:33
  • These rules only apply when using `[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:]`. The "background fetch" is a completely different pattern and thus the above is not applicable (unless, of course, you trigger a request that will be performed via a background `NSURLSessionConfiguration`). – Rob Feb 12 '15 at 13:58
  • Thanks for your example Rob, though `[self.manager setTaskDidCompleteBlock....` should probably be `[self setTaskDidCompleteBlock...` – dOM May 01 '15 at 13:53
  • for `BackgroundSessionManager`, if the call changes to upload, that is: `[BackgroundSessionManager sharedManager] uploadTaskWithRequest:req fromFile:fileURL progress:&progress completionHandler:`, Invalid parameter Assert error will occur... Any idea how to solve? – Raptor May 04 '15 at 10:59
  • Why `&progress`? Don't you mean just `progress` (assuming that's some block you've defined)? – Rob May 04 '15 at 11:05
  • Thanks for this, I can't express how much this helped me. – Nikhil.T Jul 18 '15 at 13:37
  • @Rob Most of the answers based on NSURLSession I have so far seen is from you. If you have time could you please have a look at this question too:http://stackoverflow.com/questions/31993764/nsurlsessiondownloadtask-resumes-automatically-all-task-while-in-background – Nikhil.T Aug 14 '15 at 04:46
  • hi @Rob, have you ever had any experience with starting downloads in the background from a silent push-notification from `didReceiveRemoteNotification:fetchCompletionHandler:` ?? We experience that sometimes randomly, the downloads freezes, as if iOS suspends the app... @see http://stackoverflow.com/questions/32697291/didreceiveremotenotification-fetchcompletionhandler-download-freezes – dOM Sep 22 '15 at 06:52
  • @dOM, when the background session is initiated while the app is in background then the `discretionary` property is ignored and the system considers it as `YES`. Meaning that the background tasks performed on this session will run only in good conditions (e.g. when plugged in to a power supply, connected to good WiFi etc.) – Michael Kessler Sep 30 '15 at 07:40
  • For those watching the WWDC video, the information about background transfers starts at 32:16. – ftvs Nov 06 '15 at 09:31
  • Is there any reason to use these blocks instead of the delegate methods? – Hackmodford Dec 21 '15 at 14:11
  • @Rob I have subclassed AFHTTPSessionManager and added the delegate methods. It seems to be working fine. I just wanted to know if the delegate methods maybe don't get called when download finishes and app is paused? It seems to work either way... – Hackmodford Dec 21 '15 at 14:42
  • @Hackmodford - This is not an approach that I'd recommend (you're taking a framework whose entire _raison d'être_ is to give you a block-based interface to do everything you need and you're ignoring that interface and replacing it with your own handling), but, technically, it can be done. If app not firing up when download finishes when the downloads finish, the problem is likely either a failure to specify background session or incorrect `handleEventsForBackgroundURLSession` implementation. I'd suggest posting your own question, showing code and show what debugging you've done. – Rob Dec 21 '15 at 14:56
  • @Rob I'm using your example, but when my download (a zip file in my scenario) completes, I extract the contents to a specific folder. Because I end up with multiple files, I ended up returning nil in the block. If you're interested in taking a look, I don't mind posting an example or a gist. – Hackmodford Dec 21 '15 at 17:07
  • Feel free. Or post your own question here on Stack Overflow if you want others to chime in, too. In terms of likely reasons that it's `nil` is that maybe you're using the task-specific completely blocks (which you should not do with background sessions). When your app is brought back to the foreground, all task-specific blocks are lost and you can only recreate the session-based blocks (like shown above). So, you have to determine final filenames solely from the original task request properties, not anything else in your object model. But post if this comment doesn't answer your question. – Rob Dec 21 '15 at 17:13
  • @Rob This is essentially what I came up with. https://gist.github.com/Hackmodford/abeae16b00cc3cf2b11c Would love to get your thoughts. – Hackmodford Dec 21 '15 at 18:51
  • Great answer, I'm just confused about one part. Regarding #3 in your example, "Then start your downloads", when/where is that process supposed to be initiated? In my case, users will most likely begin an upload task while the app is in the foreground, then move to the background. How do I "handoff" the process from the main call, to the background manager's call? – hgwhittle Feb 10 '16 at 15:08
  • @hgwhittle - There's no "handoff" needed. While the app is in the foreground, you just have your background session manager instantiate your upload/download task, and then call `resume`, just like shown under point #3. That's it. – Rob Feb 10 '16 at 15:53
  • @Rob thanks for all of your helpful posts my friend! – andypf Apr 12 '17 at 02:19
2

It shouldn't make any difference whether or not the callbacks are blocks or not. When you instantiate an AFURLSessionManager, make sure to instantiate it with NSURLSessionConfiguration backgroundSessionConfiguration:. Also, make sure to call the manager's setDidFinishEventsForBackgroundURLSessionBlock with your callback block - this is where you should write the code typically defined in NSURLSessionDelegate's method: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. This code should invoke your app delegate's background download completion handler.

One word of advice regarding background download tasks - even when running in the foreground, their timeouts are ignored, meaning you could get "stuck" on a download that's not responding. This is not documented anywhere and drove me crazy for some time. The first suspect was AFNetworking but even after calling NSURLSession directly, the behaviour remained the same.

Good luck!

Stavash
  • 14,244
  • 5
  • 52
  • 80
  • Hmm. Can you clarify what you mean by the claim that it doesn't matter whether the callbacks are blocks or not? For `backgroundSessionConfiguration` you must specify delegate for your background session. As the `backgroundSessionConfiguration` documentation says, "Background transfers have a few additional constraints, such as the need to provide a delegate." In fact, if you try to use download task with completion block on background session, you get a `NSGenericException`: "Completion handler blocks are not supported in background sessions. Use a delegate instead." – Rob Jan 25 '14 at 15:47
  • @Rob AFNetworking does that for you - after all it is a convenience wrapper for networking on iOS - when you specify a block via setDidFinishEventsForBackgroundURLSessionBlock, under the hood AFNetworking registers itself as a delegate so that everything works as it should. – Stavash Jan 25 '14 at 16:01
  • thank u for your answer but I'm not sure I understand what I should do. I added example to my question, please take a look if u can. – Mario Jan 25 '14 at 17:48
  • 3
    @Stavash I spent a lot of time digging through this and my apprehensions about using blocks with `AFURLSessionManager` are warranted. `NSURLSession` does not allow block-based convenience methods with background sessions for a reason. Sure, AFNetworking uses the delegate methods, but those methods call blocks the app passed as parameters to `downloadTaskWithRequest`. If you use `AFURLSessionManager` in background session, and the app terminates, AFNetworking does _not_ handle this correctly. I've created [an issue](https://github.com/AFNetworking/AFNetworking/issues/1775) on AFNetworking. – Rob Jan 26 '14 at 03:27
  • Very interesting @Rob, I'll run some tests on my end to confirm this. – Stavash Jan 27 '14 at 05:40
  • Out of the box, this seems to work on my app with AFNetworking (crashing the app by throwing an exception results in the file still being downloaded). – Stavash Jan 28 '14 at 11:34
  • @Stavash If you use the session manager blocks, it works, but I stand by my assertion that blocks passed to `downloadTaskWithRequest` are lost when the app is terminated and then restarted. (BTW, there is also a bug in which if you had multiple downloads, only the first one would be downloaded after app was restarted, but there pull requests to fix that.) – Rob Feb 03 '14 at 12:50
  • 1
    BTW, I notice that AFNetworking class reference now bears clear, unambiguous warnings about trying to use some of those task-related blocks with background sessions, encouraging one to use the session-based ones instead. I'm glad they've improved the documentation on this point! – Rob Oct 12 '14 at 04:39
-3

AFURLSessionManager

AFURLSessionManager creates and manages an NSURLSession object based on a specified NSURLSessionConfiguration object, which conforms to <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, and <NSURLSessionDelegate>.

link to documentation here documentation

Zhans
  • 273
  • 2
  • 11