AFNetworking and background translations

I'm a little confused about how to take advantage of the new iOS 7 NSURLSession and AFNetworking migration features (versions 2 and 3).

I saw a WWDC 705 - What's New in Foundation Networking session WWDC 705 - What's New in Foundation Networking , and they demonstrated a background load that continues after the application terminates or even crashes.

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

So, I am wondering how to use it with AFNetworking (if possible) to continue downloading in the background.

The problem is that AFNetworking conveniently uses the block API to execute all requests, but if the application terminates or crashes, these blocks also disappeared. So how can I complete the task?

Or maybe I missed something ...

Let me explain what I mean:

For example, my application is a photo-sharing application, let's say that I have a PhotoMessage object that represents a single message, and this object has properties such as

  • state - describe the state of loading photos.
  • resourcePath - path to the final uploaded photo file.

Therefore, when I receive a new message from the server, I create a new PhotoMessage object and start loading its 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 this PhotoMessage object in accordance with the response received.

How can I accomplish this using background wrapping? This completion block will not be called, and as a result, I cannot update newPhotoMsg .

+49
ios objective-c afnetworking afnetworking-2
Jan 25 '14 at 11:48
source share
3 answers

A few thoughts:

  • You must make sure that you have made the necessary encoding described in the section "Processing iOS background activity" in the "Download URL loading system" section. The manual says:

    If you use NSURLSession on iOS, your application will automatically restart when the download is complete. Your application method app application:handleEventsForBackgroundURLSession:completionHandler: is responsible for re-creating the corresponding session, storing the completion handler and calling this handler when the session invokes your session delegate URLSessionDidFinishEventsForBackgroundURLSession:

    This guide provides some examples of what you can do. Honestly, I think the code examples discussed in the final installment of WWDC 2013 What's New in Foundation Networking are even clearer.

  • The main implementation of AFURLSessionManager will work in conjunction with background sessions if the application is simply paused (you will see that your blocks are called up when performing network tasks, if you have done so). But, as you might have guessed, any task-specific block parameters that are passed to the AFURLSessionManager method, where you create the NSURLSessionTask to load and download, are lost "if the application is terminated or crashes."

    For downloads in the background, this is annoying (since your informational levels of progress and locks at the task level that you specified when creating the task will not be called). But if you use session level estimates (for example, setTaskDidCompleteBlock and setTaskDidSendBodyDataBlock ), it will be called correctly (if you always set these blocks when reinitializing the session manager).

    As it turns out, this block loss problem is actually more problematic for downloading phonograms, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks such as setDownloadTaskDidFinishDownloadingBlock ).

  • An alternative is to stick with the standard (non-phonic) NSURLSession , but make sure your application takes a little time to complete the download if the user leaves the application while the task is running. For example, before creating an 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 network task completion block correctly informs iOS that it has completed:

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

    This is not as strong as the NSURLSession background (for example, you have a limited amount of time), but in some cases it can be useful.




Update:

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

  • 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 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, eg 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, eg, 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 
  • Make sure that the application delegate saves the completion handler (creates an instance of the background session if 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; } 
  • Then run the download:

     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 that I do not supply any of these task-related blocks because they are not reliable for background sessions. (The initial downloads continue even after the application is completed, and these blocks have long disappeared.) You must rely on the session level to easily recreate setDownloadTaskDidFinishDownloadingBlock .

Obviously, this is a simple example (only one background session object, just saving files to a document folder using the last URL component as a file name, etc.), but hopefully it illustrates the template.

+71
Jan 26 '14 at 4:26
source share
— -

It does not matter if the callbacks are blocks or not. When you create an instance of AFURLSessionManager , be sure to create it using NSURLSessionConfiguration backgroundSessionConfiguration: Also, do not forget to call the setDidFinishEventsForBackgroundURLSessionBlock manager using the callback block - here you should write the code usually defined in the NSURLSessionDelegate method: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session . This code should call the application delegate download completion handler.

One of the tips regarding background loading tasks is that even when launched in the foreground, their timeouts are ignored, which means you can get stuck on loading that does not respond. This is not documented anywhere, and I went crazy for a while. The first suspect was AFNetworking, but even after a direct call to NSURLSession, the behavior remained the same.

Good luck

+2
Jan 25 '14 at 12:00
source share

AFURLSessionManager

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

link to documentation here documentation

-3
Jan 25 '14 at 12:05
source share



All Articles