In my App I get JSON from a Web service. This JSON contains the urls of several files that I would like to download.
I want to download each file one by one (wait for the first download to finished until the second one starts and so an and so forth) using NSURLSessionDownloadTask. I would also like to keep track of the total bytes written so I can update UI.
Thanks a lot in advance !
NSURLSessionDownloadTask's as you well know do not play very nicely with NSOperationQueues unlike their counterpart the NSURLConnection (where it could be encapsulated inside an NSOperation).
One option would be to add all your urls to an array, and then inside the completionHandler of the task, simply queue the next item.
So you might create your tasks in a loop, call a progressBlock inside each tasks completion handler, store the tasks in an array, and queue the next task inside each tasks completion handler:
- (void)addRequestsWithURLs:(NSArray *)urls
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations, NSURLSessionDownloadTask *task,NSURL *location, NSURLResponse *response, NSError *error))progressBlock {
__block NSUInteger numberOfFinishedOperations = 0;
NSUInteger totalNumberOfOperations = [urls count];
for (NSString *url in urls) {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
__block NSURLSessionDownloadTask *task = [self.session downloadTaskWithRequest:request
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
//downloadFileSomewhere
++numberOfFinishedOperations;
if (progressBlock) {
progressBlock(numberOfFinishedOperations, totalNumberOfOperations,task,destination != nil ? [NSURL fileURLWithPath:destination] : nil,response,error);
}
//queueNext
[self processCompletedTask:task];
}];
//stores an array of NSURLSessionTasks
[self.tasksWaitingToBeQueued addObject:task];
}
}
- (void)processCompletedTask:(NSURLSessionTask *)completedTask {
//clean up and queue next one
[self.tasksWaitingToBeQueued removeObject:completedTask];
nextTask = [self.tasksWaitingToBeQueued firstObject];
if (nextTask) {
[nextTask resume];
}
}
NOTE
In this example I show progress as the number of tasks completed and not the number of bytes, this is the recommended approach (its also simpler). To indicate progress using bytes you would need to know the total number of bytes to download beforehand (since you want to show a progress bar) and also implement the NSURLSession delegate and monitor the progress of each task, capture the bytes downloaded and update your block. If your server doesn't tell you the total number of bytes then you would probably need to do a HEAD request for every resource and aggregate the sizes. Personally this solution is way to complicated for what could simply be resolved by indicating progress as the number of files downloaded.
To achieve this might look something like this:
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
self.totalBytesWritten += totalBytesWritten;
NSUInteger totalProgressSoFar = self.totalBytesWritten;
NSUInteger totalExpectedBytes = self.totalExpectedBytes;
//you would need to capture some progress block locally - beware of retain cycles
self.progressBlock(totalProgressSoFar/totalExpectedBytes)
}
when you finish you should set the progressBlock to nil to prevent any retain cycles.
Related
Has anyone been successful in chaining NSURLSession background uploads?
I am trying to upload a huge video file in 5 MB parts using background upload of NSURLSession. The uploads has to be in order. The whole thing works fine in foreground. I am using AFNetwoking for this, and its a multi part upload. But when the app is in background, the first item uploads fine and starts the second one in background (in setDidFinishEventsForBackgroundURLSessionBlock of AFURLSessionManager). But it stops abruptly (my best guess is in 30 seconds, as an app woken up in background has a max lifetime of 30 sec) and then nothing happens. I expected the second session will finish in background and call up the third etc - a chain behaviour, but this just does not seem to work.
I have tried adding all file parts to a single NSURLSession in one go with a HTTPMaximumConnectionsPerHost = 1 - this works fine and uploads the full file in parts. But the file parts are picked in random order, i.e. part 1 gets uploaded, then part 5, part 3, part 10 etc …. I tried adding this in an NSOperationQueue with dependency between operations and this seems to mess up the entire thing - the upload does not work at all.
I know that the video file can be uploaded as a single file in background, but the server expects this in 5 MB parts. Hence I guess my only option is to chain uploads, or add all the parts to a NSURLSession, but make sure they are always uploaded in the order they are added.
Any help would be appreciated.
My code:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[NSString stringWithFormat:#"%d", rand()]];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
config.HTTPMaximumConnectionsPerHost = 1;
[manager setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
dispatch_async(dispatch_get_main_queue(), ^{
// Call the completion handler to tell the system that there are no other background transfers.
// completionHandler();
[self upload];
});
}];
}
- (IBAction)start:(id)sender {
[self upload];
}
-(void) upload {
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Sample" ofType:#"mp4"];
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys:#"234", #"u", #"Sample.mp4", #"f",nil];
NSMutableURLRequest *request = [serializer multipartFormRequestWithMethod:#"POST" URLString:urlString parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileURL:[NSURL fileURLWithPath:filePath] name:#"data" fileName:#"Sample.mp4" mimeType:#"video/mp4" error:nil];
} error:nil];
__block NSString *tempMultipartFile = [NSTemporaryDirectory() stringByAppendingPathComponent:#"Test"];
tempMultipartFile = [tempMultipartFile stringByAppendingString:[NSString stringWithFormat:#"%d", rand()]];
NSURL *filePathtemp = [NSURL fileURLWithPath:tempMultipartFile];
__block NSProgress *progress = nil;
[serializer requestWithMultipartFormRequest:request writingStreamContentsToFile:filePathtemp completionHandler:^(NSError *error) {
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePathtemp progress:&progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
NSLog(#"Request--> %#.\n Response --> %# \n%#", request.URL.absoluteString ,responseObject, error? [NSString stringWithFormat:#" with error: %#", [error localizedDescription]] : #""); //Lets us know the result including failures
[[NSFileManager defaultManager] removeItemAtPath:tempMultipartFile error:nil];
}];
[uploadTask resume];
[manager setTaskDidSendBodyDataBlock:^(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
NSLog(#"uploading");
}];
}];
}
Well, finally I reached out to Apple for clarifications on chaining background uploads - This is not possible in iOS.NSURLSession has a Resume Rate Limiter which prevents apps from executing chained tasks in background as explained in https://forums.developer.apple.com/thread/14854. Instead, apple suggests batch transfers or other options like https://forums.developer.apple.com/thread/14853. The other thing I was asking was to order the multiple tasks in upload queue - i.e, force NSURLSession to upload tasks in the order in which they are added. As pointed by dgatwood, using an NSOperationQueue is not possible and Apple also mentioned the same.As mentioned by eskimo in response mail "NSURLSession does not guarantee to run your requests in order and there’s no way to enforce that." So I am pretty much left option less on my original problem.
An NSOperationQueue goes away when your app does, which isn't too long after you put it into the background. So that's not going to work very well.
Instead, store a list of files remaining to upload, in order—either in a file on disk or in NSUserDefaults, depending on your personal preference. Then, use an upload task in a background session to start the first task. When it finishes, if your app isn't running, it should automatically get relaunched in the background to handle the data.
To support this behavior, in your application:handleEventsForBackgroundURLSession:completionHandler: method, re-create the background session just like you did originally, and store the completion handler.
Shortly thereafter, your delegate methods for the request should be called just as though your app were still running when the download finished. Among other things those methods can provide your app with the response data from the server, the response object (for checking the status code), etc.
When you get the didCompleteWithError delegate call (which is nil on success, IIRC), if the transfer failed, try it again or whatever. If it succeeded, start uploading the next one and update your list of files on disk.
Either way, when your session delegate's ** URLSessionDidFinishEventsForBackgroundURLSession:** method is called, call the handler you stored earlier, roughly like this:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
storedHandler();
}];
By calling the completion handler, you're telling the OS that you don't need to keep running.
Rinse, repeat.
If your app is still running when the request completes, everything happens just as described above, except that you don't get the application:handleEventsForBackgroundURLSession:completionHandler: or URLSessionDidFinishEventsForBackgroundURLSession: calls, which means you don't have to store the completion handler or call it.
See URL Session Programming Guide for details.
iOS 8, XCode 6.3.2
I want to download multiple files serially.
In the wake of the Push notification, APP will start BackgroudDownload by NSURLSessionDownloadTask.
After the First BackgroudDownload process has been completed, APP want to start Second process, but Second BackgroudDownload process does not start.
Code is below
// This method is called by Push Notification
- (void)startBackgroundDownload
{
// Session
NSURLSessionConfiguration *configFirst = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.test.first"];
sessionFirst = [NSURLSession sessionWithConfiguration:configFirst delegate:self delegateQueue:nil];
NSURLSessionConfiguration *configSecond = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.test.second"];
sessionSecond = [NSURLSession sessionWithConfiguration:configSecond delegate:self delegateQueue:nil];
// Start First Download
NSURLRequest *requestFirst = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://xxxxx/first.zip"]];
NSURLSessionDownloadTask *downloadTaskFirst = [sessionFirst downloadTaskWithRequest:requestFirst];
[downloadTaskFirst resume];
}
// Finish Download
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
if (session == sessionFirst) {
NSURLRequest *requestSecond = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://xxxxx/second.zip"
NSURLSessionDownloadTask *downloadTaskSecond = [sessionSecond downloadTaskWithRequest:requestSecond];
[downloadTaskSecond resume];
} else if (session == sessionSecond) {
NSLog(#"all finish");
}
}
The First is successful, and the Second is fail (not start).
I want advice to pursue the cause.
Thank you for any help you can provide.
downloading task is divide in perfect part like as follow.
First make one array of zip files which you want to download.
Initialise session object
Write one method which can get URL and "startDownloading"
In delegate method (successful download) called unzip that file. remove first object of zip array and again called "startDownloading" method and its call until your array count is greater than zero
I hope you will understand what I want to explain here.
I have video on my server and i want to know in how much time my video requires to download from server in the device. So that i can calculate the download time and check if server is slow or not. Any help would be appreciated.
Here is my code:
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:url] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:[[Configuration sharedInstance] fatchTimeOut]];
[GMURLConnection sendAsynchronousRequest:request queue:self.operationQueue type:type withSubType:subType completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
//**********************Success Handling ********************
if (type == OperationTypeMeta) {
failCount = 0;
NSError *error = nil;
NSArray *serverResponse=[NSArray array];
if(data)
serverResponse = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
}];
You cannot use sendAsynchronousRequest and monitor connection status. As per apple docs:
To retrieve the contents of a URL using a completion handler block: If
you do not need to monitor the status of a request, but merely need to
perform some operation when the data has been fully received, you can
call sendAsynchronousRequest:queue:completionHandler:, passing a block
to handle the results.
So you have to implement NSURLConnection delegate. If you like block style(as I really really do), just create new class which implement delegate methods and takes NSURLRequest, status block and completion block as params and call them in appropriate delegate methods. This way you will have block-styled asynchronous requests with status updates.
If you do not know how to measure remaining time - each time didReceiveData: method is called append data length to class variable, and calculate how much time passed since last method receive. This way you have velocity(bytes per second). Knowing how big is your downloaded file just divide remaining size to your velocity and you'll get remaining time. There are some neat algoritms out there to get neat/steady remaining time instead of just jumping from 30secs to 2 mins(if we assume our internet connection is tricky). I've personally used #Ben Dolman's solution posted over here:
averageSpeed = SMOOTHING_FACTOR * lastSpeed + (1-SMOOTHING_FACTOR) * averageSpeed;
With some tweaking and playing with SMOOTHING_FACTOR I've accomplished great results.
I suggest you to use AFHTTPRequestOperation. Like this:
AFHTTPRequestOperation* operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead){
}];
You may use NSURLConnection with its delegate.
When download file first you get response from server which has total file size
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
_expectedContentLength = [response expectedContentLength];
if (_expectedContentLength==NSURLResponseUnknownLength)
_expectedContentLength = 0;
}
After in - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data you may calculate downloading time from time spent to download portion of file and whole file size
I'm using synchronous requests for the first time and would love some help. (The code I'm writing is solely for my own use, and given its purposes synchronous requests are not a problem.)
The code gets data from a web page in a series, manipulates the data, moves on to the next page in the series, manipulates THAT data, and so on. I'm using a synchronous request because I need the connection to finish loading and the data to be manipulated before the function loops to the next page.
Here's my looping code:
-(NSData *)myMethod {
NSString *string;
NSData *data;
for (int x = 1; x<100; x++) {
string = [NSString stringWithFormat:#"http://www.blahblah.com/%d",(x)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:string]];
NSURLResponse *response = nil;
NSError *error = nil;
data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
}
return data;
}
When I was using connectionWithRequest, I just put the code to manipulate the data in connectionDidFinishLoading and it worked fine. But with sendSynchronousRequest, even though NSLog shows that the loop code is looping, the code in connectionDidFinishLoading never runs.
How can I fix this?
(Or am I taking the wrong approach completely?)
Here's how to take #nhgrif's good advice to perform asynch and preserve all of the results.
- (void)doRequest:(NSInteger)requestIndex gatheringResultsIn:(NSMutableArray *)array completion:(void (^)(void))completion {
if (requestIndex < 100) {
NSString *string = [NSString stringWithFormat:#"http://www.blahblah.com/%d",(requestIndex)];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:string]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data) [array addObject:data];
[self doRequest:requestIndex+1 gatheringResultsIn:array completion: completion];
}];
} else {
completion();
}
}
This will run 100 requests indexed 0..99 placing the results in a mutable array. Call it like this:
NSMutableArray *results = [NSMutableArray array];
[self doRequest:0 gatheringResultsIn:results completion:^{
NSLog(#"100 NSData objects should be here: %#", results);
}];
connectionDidFinishLoading is an NSURLConnection delegate method for when you've sent asynchronous requests. Normally, you'd implement this method to get the data that loaded, but you don't need to do this, as it's returned synchronously and assigned to your data variable.
I will note however, you are definitely taking a poor approach here.
First of all, if you'd use asynchronous requests here, you could query all 100 URLs as basically the same time and let them return in their own time.
But what's more problematic is what actually happens with your code.
We create a URL, send the synchronous request, and when it finishes, assign the return to data.
... then we loop. And do this 99 times. 99 times we make this synchronous request (to a different URL each time) and overwrite the data that the previous request loaded. And after the 100th time, we exit the loop and return the data we downloaded in the final request.
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.
A couple of thoughts:
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.
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).
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.
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
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;
}
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.
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!
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