Downloading images when the app is in idle state usingNSURLSession iOS - ios

I have a requirement to download some of the large images to the app while the app is in idle state.I am planing to make it done using NSURLSession.Tutorials and the sample code available worked and confirmed the background download is possible.Is this the best method for my requirement?Also what happens if the app is removed from the background when the download is not completed and only several bytes are got.Can i resume the download from where it stopped?Again can i use this in iOS6?These are the delegate methods i am using.
- (NSURLSession *)backgroundSession
{
/*
Using disptach_once here ensures that multiple background sessions with the same identifier are not created in this instance of the application. If you want to support multiple background sessions within a single process, you should create each session with its own identifier.
*/
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
BLog();
/*
Report progress on the task.
If you created more than one task, you might keep references to them and report on them individually.
*/
if (downloadTask == self.downloadTask)
{
double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
BLog(#"DownloadTask: %# progress: %lf", downloadTask, progress);
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = progress;
});
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL
{
BLog();
/*
The download completed, you need to copy the file at targetPath before the end of this block.
As an example, copy the file to the Documents directory of your app.
*/
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
NSURL *documentsDirectory = [URLs objectAtIndex:0];
NSURL *originalURL = [[downloadTask originalRequest] URL];
NSURL *destinationURL = [documentsDirectory URLByAppendingPathComponent:[originalURL lastPathComponent]];
NSError *errorCopy;
// For the purposes of testing, remove any esisting file at the destination.
[fileManager removeItemAtURL:destinationURL error:NULL];
BOOL success = [fileManager copyItemAtURL:downloadURL toURL:destinationURL error:&errorCopy];
if (success)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithContentsOfFile:[destinationURL path]];
self.imageView.image = image;
self.imageView.hidden = NO;
self.progressView.hidden = YES;
});
}
else
{
/*
In the general case, what you might do in the event of failure depends on the error and the specifics of your application.
*/
BLog(#"Error during the copy: %#", [errorCopy localizedDescription]);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
BLog();
if (error == nil)
{
NSLog(#"Task: %# completed successfully", task);
}
else
{
NSLog(#"Task: %# completed with error: %#", task, [error localizedDescription]);
}
double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive;
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = progress;
});
self.downloadTask = nil;
}
/*
If an application has received an -application:handleEventsForBackgroundURLSession:completionHandler: message, the session delegate will receive this message to indicate that all messages previously enqueued for this session have been delivered. At this time it is safe to invoke the previously stored completion handler, or to begin any internal updates that will result in invoking the completion handler.
*/
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
APLAppDelegate *appDelegate = (APLAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
NSLog(#"All tasks are finished");
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
BLog();
}

NSURLSession API is available from ios7.
Resuming Data in NSURLSession-n some cases, you can resume a download that was canceled or that failed while in progress. To do so, first make sure your original download doesn’t delete its data upon failure by passing NO to the download’s setDeletesFileUponFailure: method. If the original download fails, you can obtain its data with the resumeData method. You can then initialize a new download with the initWithResumeData:delegate:path: method. When the download resumes, the download’s delegate receives the download:willResumeWithResponse:fromByte: message.
You can resume a download only if both the protocol of the connection and the MIME type of the file being downloaded support resuming. You can determine whether your file’s MIME type is supported with the canResumeDownloadDecodedWithEncodingMIMEType: method
If you schedule the download in a background session, the download continues when your app is not running. If you schedule the download in a standard or ephemeral session, the download must begin anew when your app is relaunched.
During the transfer from the server, if the user tells your app to pause the download, your app can cancel the task by calling the cancelByProducingResumeData: method. Later, your app can pass the returned resume data to either the downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler: method to create a new download task that continues the download.
If the transfer fails, your delegate’s URLSession:task:didCompleteWithError: method is called with an NSError object. If the task is resumable, that object’s userInfo dictionary contains a value for the NSURLSessionDownloadTaskResumeData key. Your app should use reachability APIs to determine when to retry, and should then call downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler: to create a new download task to continue that download.

Related

NSURLSessionDownloadTask not deleting the file when the app is closed by the user and the task was still active

I have a NSURLSession and a NSURLSessionDownloadTask configured for downloading a file in background, if the download task in canceled by the user all the data is deleted and the storage space the file was using is freed, but if the app is closed from the multitasking dock the download task is terminated and gives an error but is not deleting the data and the temporal data for the file is still occupying storage space and is never freed. What do i need to do in order to free the space ?
This is my NSURLSession configuration and error handling:
- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration;
if ([[UIDevice currentDevice].systemVersion hasPrefix:#"7"]) configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.visyon.pr"];
else configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.visyon.pr"];
configuration.sessionSendsLaunchEvents =YES;
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session; }
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error == nil) {
NSLog(#"Task: %# completed successfully", task );
} else {
// [self hideActivity];
// [self showAlertBoxErrorDownload];
NSLog(#"Task: %# completed with error: %#, %lu", task, [error localizedDescription], (long)error.code);
} self.downloadTask = nil; }
ok after more than 10 days of try and fail I found the solution, first of all there are two case scenarios when the user close the app and there is an active download in background, please take in to account that for this cases the download task is never intended to be resumed.
scenario 1. when the user close the app but is still active, the - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error is called and the temporal file that was using for the download goes directly to the temp directory of the app, in this case the - (void)applicationWillTerminate:(UIApplication *)application is called but the delegate of the download task is called first. The solution is this case is to implement code to clean the temp directory every time the app is opened or let iOS to clean the temp folder, it is explained here when iOS is going to clean the temp file:
When does iOS clean the local app ./tmp directories?
scenario 2. when the user close the app in background mode the app is terminated and the next time the app is opened the - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error is called and the temporal file that was using for the download is stored in the NSCachesDirectory in the next directory:
var/mobile/Containers/Data/Application/CA21-B6E8-3305A39/Library/Caches/com.apple.nsurlsessiond/Downloads/com.xxx.xxx/CFNetworkDownload_M5o8Su.tmp
The temporal file will be move to the temporal directory of the app the next time there is a new download.
the solution here is to implement code to delete all temporal files from Caches/com.apple.nsurlsessiond/Downloads/com.xxx.xxx/ as soon as the - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error is launched.
Here is the code in order to delete the temporal files from NSCachesDirectory:
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[path stringByAppendingPathComponent:#"/com.apple.nsurlsessiond/Downloads/com.xxx.xxx/"] error:nil];
for (NSString *string in array) {
[[NSFileManager defaultManager] removeItemAtPath:[path stringByAppendingPathComponent:[NSString stringWithFormat:#"/com.apple.nsurlsessiond/Downloads/com.xxx.xxx/%#", string]] error:nil];
}
But because it only work for this scenario its better to implement code to clean the temporal directory of the app every time is launched or let iOS to clean the temp folder so it will work for both scenarios.
Here is the code on how to clean the temporal directory of the app:
NSString *path = NSTemporaryDirectory();
NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];
for (NSString *string in array) {
[[NSFileManager defaultManager] removeItemAtPath:[path stringByAppendingPathComponent:string] error:nil];
}
First point is if you are configure nsurlsession with background configuration type. Then this type of session till runing if user kill the app.
Second point if you want to clear space then you need to stop that session and manually write code for deleting temporary file from tmp folder.
Change session configuration type.
There's a method in the AppDelegate called - (void)applicationWillTerminate:(UIApplication *)application. You can try either calling [NSURLSessionDownloadTask cancel]; from there; or try setting a flag to indicate to the app there was a download in progress when the app was closed and delete the data the next time the app is opened.

NSURLSessionUploadTask "finishes" but no network traffic

I have an application that uploads several pictures held in an array of TSPhoto objects.
When I start the upload, the delegate method eventually fires to say that it's done, but the app never hits the server and Instruments is showing no network traffic:
_session = [self backgroundSession];
for(TSPhoto *photo in _picsetPhotos) {
NSURL *uploadURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#upload", #"https://example.com/"]];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:uploadURL];
[req setHTTPMethod:#"POST"];
NSLog(#"req dump: %#", req);
// Add it to the queue
NSURLSessionUploadTask *uploadTask = [_session uploadTaskWithRequest:req fromFile:[NSURL fileURLWithPath:photo.fullImagePath]];
// "Start" it
NSLog(#"Enqueueing %#", uploadTask);
[uploadTask resume];
NSLog(#"Post-resume");
}
-backgroundSession:
-(NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"TSBackgroundUploader"];
configuration.HTTPMaximumConnectionsPerHost = 1;
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
I have implemented all of the appropriate delegate methods:
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(#"Finished all uploads!");
[[[UIAlertView alloc] initWithTitle:#"Upload Completed" message:#"Your album has been created." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil] show];
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
dispatch_async(dispatch_get_main_queue(), ^{
[_uploadProgress setProgress:
(double)totalBytesSent /
(double)totalBytesExpectedToSend animated:YES];
});
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
[_uploadProgress setProgress:0.5];
// Did we succeed?
if(!error) {
NSLog(#"Upload done");
} else {
NSLog(#"%#", error.description);
}
});
The progress bar also acts really wonky during all of this, rising and falling until eventually reaching 100.
Any clue what might be going on?
edit:
Here's the log after I added some more NSLog statements. The tasks call the delegate in the reverse order in which they were started:
2015-05-04 09:55:42.492 TS[348:95052] Starting background upload
2015-05-04 09:55:42.493 TS[348:95052] req dump: <NSMutableURLRequest: 0x1740138b0> { URL: https://example.com/upload }
2015-05-04 09:55:42.512 TS[348:95052] Enqueueing <__NSCFBackgroundUploadTask: 0x155d804c0>{ taskIdentifier: 3 }
2015-05-04 09:55:42.513 TS[348:95052] Post-resume
2015-05-04 09:55:42.513 TS[348:95052] req dump: <NSMutableURLRequest: 0x17400de00> { URL: https://example.com/upload }
2015-05-04 09:55:42.524 TS[348:95052] Enqueueing <__NSCFBackgroundUploadTask: 0x155e68db0>{ taskIdentifier: 4 }
2015-05-04 09:55:42.525 TS[348:95052] Post-resume
2015-05-04 09:56:01.726 TS[348:95052] ID: 4
2015-05-04 09:56:01.726 TS[348:95052] Upload done
2015-05-04 09:56:02.920 TS[348:95052] ID: 3
2015-05-04 09:56:02.920 TS[348:95052] Upload done
You don't see any network activity because the background session transfers data on a separate daemon that is managed by the system.
This is an excerpt from Apple Documentation:
"Once configured, your NSURLSession object seamlessly hands off upload and download tasks to the system at appropriate times. If tasks finish while your app is still running (either in the foreground or the background), the session object notifies its delegate in the usual way. If tasks have not yet finished and the system terminates your app, the system automatically continues managing the tasks in the background. If the user terminates your app, the system cancels any pending tasks."
You can read more here:
https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html
Part of me feels really foolish and part of me is really confused.
It turns out that data was being sent. I was working with an incomplete upload endpoint, and when I finished implementing the endpoint, the pictures went through fine!
But for the entire transaction - uploading 10MB worth of pictures - the only network traffic that's showing in the debugger is a few KB for a smaller request I'm making before starting the tasks:
I guess I'll just file this one under "weird shit that Xcode does" and move on.

Why is NSURLSession slower than cURL when downloading many files?

I've been using cURL to download about 1700+ files -- which total to about ~290MB -- in my iOS app. It takes about 5-7 minutes on my Internet connection to download all of them using cURL. But since not everyone has fast internet connection (especially when on the go), I decided to allow the files to be downloaded in the background, so that the user can do other things while waiting for the download to finish. This is where NSURLSession comes in.
Using NSURLSession, it takes about 20+ minutes on my Internet connection to download all of them while the app is in foreground. I don't mind it being slow when the app is in background, because I understand that it is up to the OS to schedule the downloads. But it's a problem when it's slow even when it's in foreground. Is this the expected behaviour? Is it because of the quantity of the files?
In case I'm not using NSURLSession correctly, here's a snippet of how I'm using it:
// Initialization
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"<my-identifier>"];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 40;
backgroundSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
// ...
// Creating the tasks and starting the download
for (int i = 0; i < 20 && queuedRequests.count > 0; i++) {
NSDictionary *requestInfo = [queuedRequests lastObject];
NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithURL:[NSURL URLWithString:requestInfo[#"url"]]];
ongoingRequests[#(downloadTask.taskIdentifier)] = requestInfo;
[downloadTask resume];
[queuedRequests removeLastObject];
NSLog(#"Begin download file %d/%d: %#", allRequests.count - queuedRequests.count, allRequests.count, requestInfo[#"url"]);
}
// ...
// Somewhere in (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
// After each download task is completed, grab a file to download from
// queuedRequests, and create another task
if (queuedRequests.count > 0) {
requestInfo = [queuedRequests lastObject];
NSURLSessionDownloadTask *newDownloadTask = [backgroundSession downloadTaskWithURL:[NSURL URLWithString:requestInfo[#"url"]]];
ongoingRequests[#(newDownloadTask.taskIdentifier)] = requestInfo;
[newDownloadTask resume];
[queuedRequests removeLastObject];
NSLog(#"Begin download file %d/%d: %#", allRequests.count - queuedRequests.count, allRequests.count, requestInfo[#"url"]);
}
I've also tried using multiple NSURLSession, but it's still slow. The reason I tried that is because when using cURL, I create multiple threads (around 20), and each thread will download a single file at a time.
It's also not possible for me to reduce the number of files by zipping it, because I need the app to be able to download individual files since I will update them from time to time. Basically, when the app starts, it will check if there are any files that have been updated, and only download those files. Since the files are stored in S3, and S3 doesn't have zipping service, I could not zip them into a single file on the fly.
As mentioned by Filip and Rob in the comments, the slowness is because when NSURLSession is initialized with backgroundSessionConfigurationWithIdentifier:, the download tasks will be executed in the background regardless if the app is in the foreground. So I solved this issue by having 2 instances of NSURLSession: one for foreground download, and one for background download:
NSURLSessionConfiguration *foregroundSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
foregroundSessionConfig.HTTPMaximumConnectionsPerHost = 40;
foregroundSession = [NSURLSession sessionWithConfiguration:foregroundSessionConfig
delegate:self
delegateQueue:nil];
[foregroundSession retain];
NSURLSessionConfiguration *backgroundSessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.terato.darknessfallen.BackgroundDownload"];
backgroundSessionConfig.HTTPMaximumConnectionsPerHost = 40;
backgroundSession = [NSURLSession sessionWithConfiguration:backgroundSessionConfig
delegate:self
delegateQueue:nil];
[backgroundSession retain];
When the app is switched to background, I simply call cancelByProducingResumeData: on each of the download tasks that's still running, and then pass it to downloadTaskWithResumeData::
- (void)switchToBackground
{
if (state == kDownloadManagerStateForeground) {
[foregroundSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
[downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithResumeData:resumeData];
[downloadTask resume];
}];
}
}];
state = kDownloadManagerStateBackground;
}
}
Likewise, when the app is switched to foreground, I do the same but switched foregroundSession with backgroundSession:
- (void)switchToForeground
{
if (state == kDownloadManagerStateBackground) {
[backgroundSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
[downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
NSURLSessionDownloadTask *downloadTask = [foregroundSession downloadTaskWithResumeData:resumeData];
[downloadTask resume];
}];
}
}];
state = kDownloadManagerStateForeground;
}
}
Also, don't forget to call beginBackgroundTaskWithExpirationHandler: before calling switchToBackground when the app is switched to background. This is to ensure that the method is allowed to complete while in background. Otherwise, it will only be called once the app enters foreground again.

AFNetworking and background transfers

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

download task is null for the first request when when in backgroundfetch IOS

I was wondering if you have seen this or might have some ideas as to why I see the following behavior in my code: I have an NSURLsession with background config. I initiate periodic download task when the program runs in the Foreground, and everything works. WhenI simulate backgroundfetch (in xcode), my task gets a null value (eventhough the request and the session are not null). of course in this case, my session delegate never gets fired to do completionhandler. if I simulate subsequent background fetches, they all work afterward. at this point, if I bring the app to the foreground in the simulator, and I simulate another backgroundfetch, the symptoms star all over. I am using this code in my appdelegate class.
your help is greatly appreciated.
- (NSURLSession *)FlickrSession
{
if(!_FlickrSession)
{
NSLog(#"setting new FlickrSession");
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:FLICKR_SESSION];
configuration.allowsCellularAccess = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_FlickrSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
_FlickrSession.sessionDescription = FLICKR_SESSION;
NSLog(#" new self is %#", _FlickrSession);
NSLog(#"queue in session %#", dispatch_get_current_queue());
});
}
return _FlickrSession;
}
-(void) startFlickrFetch
{
// initialize session config and the background session
[self.FlickrSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks)
{
if(![downloadTasks count])
{
NSLog(#"new downloadtask session %#", self.FlickrSession.sessionDescription);
NSURLRequest *request = [NSURLRequest requestWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]];
// NSURLSessionDownloadTask *task = [self.FlickrSession downloadTaskWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]];
NSURLSessionDownloadTask *task = [self.FlickrSession downloadTaskWithRequest:request];
task.taskDescription = FLICKR_DOWNLOAD_TASK;
NSLog(#"new request %#", request);
NSLog(#"new downloadtask %#", task);
[task resume];
//task?[task resume]:[self fireBackgroungFetchCompletionHandler];;
//[self fireBackgroungFetchCompletionHandler];
NSLog(#"queue in task %#", dispatch_get_current_queue());
}
else
{
NSLog(#"resuming old downloadtask %d", [downloadTasks count]);
for(NSURLSessionDownloadTask *task in dataTasks) [task resume];
}
}];
NSLog(#"queue outside the block %#", dispatch_get_current_queue());
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// open the file, if there is no managedContext. this is the case wjere the application was launched directly by user
// it did not come from the bckground state;
//need to enable background fetch
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
//[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentChangedState) name:UIDocumentStateChangedNotification object: self.document];
NSLog(#"in application didfinishlaunching");
[self openDatabaseFile];
[self startFlickrFetch];
return YES;
}
-(void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
self.backgroundFetchCompletionHandler = completionHandler;
if(self.document.documentState == UIDocumentStateNormal)
{
//[self openDatabaseFile];
NSLog(#"in performFetchWithCompletionHandler");
[self startFlickrFetch];
}
}
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
self.completionhandler = completionHandler;
NSLog(#"handle event for backgroundURLsession***********");
}
This could have something to do with a strange interaction of the background fetch and a background session, in which case I have no advice.
However, in the background, iOS doesn't know to wait for async calls, e.g. getTasksWithCompletionHandler. You can solve this by wrapping those calls with a UIBackgroundTaskIdentifier†-based [UIApplication] begin/end task (in this case, with the "end (app) task" inside the "get (session) tasks completion handler" block).
But if all you need is a count, here's what I did, which I think is simpler:
Create an ivar:
NSMutableSet *activeTaskIDs;
When you create a task, add it to the set:
[activeTaskIDs addObject:#(task.taskIdentifier)];
When the task completes, remove it.
You can get your count from there, no async.
† Confusingly, a different kind of task. I differentiate with the terms "app tasks" vs. "session tasks".

Resources