While my app is in the background, I want to upload many files using NSURLSessionUploadTask.
With a suitably configured NSURLSession object, the API for queueing background uploading is:
NSURLSessionUploadTask *dataTask = [sessionObj uploadTaskWithRequest:urlRequest
fromFile:localFilePath];
[dataTask resume];
When the upload completes -- success or failure -- I get this callback in the background:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
During the above callback, I queue another upload with
-uploadTaskWithRequest:fromFile:.
But after some files are uploaded, the callbacks stop, and so does the uploading.
Is there something I'm missing to keep uploads going? E.g. do I need to put some extra code on this callback to keep the uploads going?
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
AppDelegate *appDelegate = (id)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
}
Note: I have already read this related SO question, but it didn't help.
Keep the task queue topped off
I've found that, in the background, you should avoid letting the task queue reach 0 tasks until you are really "done". Instead, you get better continuity if you always keep few tasks in the queue at all times. For instance, when the number of tasks gets down to 3, add 3 more.
Protip: for a count of active tasks, you're better off with your own tally, rather than clumsy async -[NSURLSession getTasksWithCompletionHandler:]. I add/remove the background task ID (as #(taskID)) to an NSSet (e.g. NSSet *activeTaskIDs), and use the count from that. I spell this out here.
Bracket async calls
If you do anything async during the the didComplete... callback, you must surround that with UIApplication's -beginBackgroundTaskWithExpirationHandler: and -endBackgroundTask:. Otherwise, it looks like you've popped the stack, and iOS will kill you.
Related
How do photo apps upload EVERYTHING from the CameraRoll in the background?
I have the need to upload 100s of photos in the background based on date range. My app is currently using NSURLSession with the following code (I think...) But for this to work, my task scheduler has to copy the JPG to a file in App storage (see: Background Upload With Stream Request Using NSUrlSession in iOS8) before the App goes into background. For 100s of photos this takes too much time and storage.
Is there a way to use a "streams" approach, or to reliably schedule additional NSURLSession tasks from the background? My developer says that CameraRoll photos that are potentially in iCloud would cause background scheduling to fail.
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
NSString *identifier = task.originalRequest.allHTTPHeaderFields[#"X-Image-Identifier"];
NSDictionary *d = [self sessionInfosDictionary];
NSURLSessionTaskInfo *info = d[identifier];
double p = (double)totalBytesSent/(double)totalBytesExpectedToSend;
info.progress = p;
[self saveSessionInfos:d];
for (id<PhotosUploaderDelegate>delegate in _delegates) {
if ([delegate respondsToSelector:#selector(photoUploader:didUploadDataForAssetWithIdentifier:totalBytesSent:totalBytesExpectedToSend:)]) {
[delegate photoUploader:self didUploadDataForAssetWithIdentifier:identifier totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend];
}
}
}
The task is not trivial, there is a lot of work to do.
Start from [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:] and +[NSURLSession uploadTaskWith...] methods.
You'll see that the tricky part is recovering from upload errors. You'll need to track each background upload in your application, by checking -[NSURLSession getTasksWithCompletionHandler:]. But first start from the beginning, with background session configurations and upload tasks.
I haven't tried it but maybe you could copy the file and start a new upload task in the background session callback? This way you might be able to copy and upload the files one at a time.
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
// copy file and schedule new upload task
}
I'm trying to upload my iOS photo's from the camera roll to an external server via a background fetch call. To accomodate this, i loop through ALAssetsLibrary in the background fetch to look for new photos. When new photos are found or all photo's in case of a new device, i initiate a background transfer for that photo. My plan was to start a NSURLSession and add a few tasks per fetch.
This works. The files get uploaded. But the callbacks are inconsistent. After simulating a lot of background fetches, one in a hundred times the didCompleteWithError callback isn't fired. But the biggest problem is that the Tasks don't match a lot of the times. when i check the task Identifier after creating a single task with:
NSURL *theURL = [NSURL fileURLWithPath:fullFileName isDirectory:NO];
NSURLSessionUploadTask *uploadTask = [_session uploadTaskWithRequest:request fromFile:theURL];
NSLog(#"Task id at start: %d", [uploadTask taskIdentifier]);
And in the callback:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
NSLog(#"Didcomplete - task_id: %d", [task taskIdentifier]);
...
My output is:
2013-12-31 14:02:36.628 Project[18685:1303] Didcomplete - task_id: 30
2013-12-31 14:02:36.768 Project[18685:70b] Task id at start: 31
I guess i already read somewhere that the output does not have to be linear because of the background tasks. But because the identifiers differ, i can't match the output to the task and proccess the background task correctly after it's finished.
Anyone has an idea what could cause this behavior? Or what i could try?
This seems to be a simulator issue. When testing on a real device, the task identifiers match up in the didcomplete callback and proper processing of the request is possible.
I have a requirement to download a number of files (around 500). I have an array containing all the urls of these files, I wanted to use NSURLSession so that i can support background downloading too.
I cant think of correct way to achieve this. If i am initiating next file download after one is completed then background downloading will not work.
shall I creating multiple downloading tasks and initiate?
Please suggest me how to achieve this.
Edit:
First, sorry for late response and here is a solution for your problem. Begin with downloading Apple's Simple Background Transfer sample. Then you will see the URLSessionDidFinishEventsForBackgroundURLSession method in view controller. You can modify this method for calling another download task like below sample and I think this is what you want to do.
There is also a comment over this method like this the session delegate will receive this message to indicate that all messages previously enqueued for this session have been delivered. So creating a queue for your requests could be better solution then this.
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
APLAppDelegate *appDelegate = (APLAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
[self start:nil];
}
NSLog(#"All tasks are finished");
}
I was having problems with this. My app had to update itself and download news videos in the background using BACKGROUND FETCH to get json list of files then firing off n number of webservice calls to download these files using BACKGROUND TRANSFER
[NSURLSessionConfiguration backgroundSessionConfiguration:
For each file I was creating one NSSession and one NSURLSessionDownloadTask.
file 1 - NSSession1 > NSURLSessionDownloadTask1
file 2 - NSSession2 > NSURLSessionDownloadTask2
file 3 - NSSession3 > NSURLSessionDownloadTask3
This woke fine when the app was in the foreground.
But I had problems when the app was in background and woken by BACKGROUND FETCH
One file would download and then it would halt.
It was like only the first NSSession1 was executed.
It may have been that iOS was waiting till device was idle again to run next session but this was too slow
I got it working by having one NSSession and attaching all NSURLSessionDownloadTask3
NSURLSession * backgroundSession_ =
for(url to call){
create NSURLSessionDownloadTask1 (set its session:backgroundSession_)
create NSURLSessionDownloadTask2 (set its session:backgroundSession_)
create NSURLSessionDownloadTask3 (set its session:backgroundSession_)
}
Be careful when doing this
call NSSession finishTasksAndInvalidate not invalidateAndCancel
//[session invalidateAndCancel];
[session finishTasksAndInvalidate];
invalidateAndCancel will stop the session and not finish the other download tasks
Ok, so I was looking at the SimpleBackgroundFetch example project, and it uses the following in the App Delegate:
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:someTimeInSeconds];
//^this code is in didFinishLaunchingWithOptions
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
//do something + call completionHandler depending on new data / no data / fail
}
So, basically I assume, that I call my app's server here, to get some data.
But then I saw the NSURLSession docs, and it had methods like these
– downloadTaskWithURL:
and it said the following:
This API provides a rich set of delegate methods for supporting
authentication and gives your app the ability to perform background
downloads when your app is not running or, in iOS, while your app is
suspended.
So what's the difference between these two APIs? And what should I use if I want to download some data from my app's server every now and again?
I just wasn't sure about the difference between the two, so I just thought I should get my doubts clarified here. Go StackOverflow!
These are completely different things.
Background Fetch: System launches your app at some time (heuristics) and your job is to start downloading new content for user.
NSURLSession: Replacement for NSURLConnection, that allows the downloads to continue after the app is suspended.
The application delegate is for storing the completion handler so you can call it when your download is finished.
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSLog(#"Handle events for background url session");
self.backgroundSessionCompletionHandler = completionHandler;
}
and call the handler
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
WebAppDelegate *appDelegate = (WebAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
NSLog(#"All tasks are finished");
}
NSURLSession:Allows to uploading and downloading in the background mode and suspend mode of application
Background Fetch:Happens according to volume of the data and duration of previous data transferring process.Only last for 30s.
So you confirm a background URLSession endowed with a delegate should be called, while a normal dataTask with block may not be?
I have some problems on elaborating a useful strategy to support background for NSOperationQueue class. In particular, I have a bunch of NSOperations that perform the following actions:
Download a file from the web
Parse the file
Import data file in Core Data
The operations are inserted into a serial queue. Once an operation completes, the next can start.
I need to stop (or continue) the operations when the app enters the background. From these discussions ( Does AFNetworking have backgrounding support? and Queue of NSOperations and handling application exit ) I see the best way is to cancel the operations and the use the isCancelled property within each operation. Then, checking the key point of an operation against that property, it allows to roll back the state of the execution (of the running operation) when the app enters background.
Based on Apple template that highlights background support, how can I manage a similar situation? Can I simply cancel the operations or wait the current operation is completed? See comments for details.
- (void)applicationDidEnterBackground:(UIApplication *)application
{
bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
// Do I have to call -cancelAllOperations or
// -waitUntilAllOperationsAreFinished or both?
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// What about here?
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
}
Thank you in advance.
Edit
If NSOperation main method perform the code below, how is it possible to follow the Responding to Cancellation Events pattern?
- (void)main
{
// 1- download
// 2- parse
// 2.1 read file location
// 2.2 load into memory
// 3- import
// 3.1 fetch core data request
// 3.2 if data is not present, insert it (or update)
// 3.3 save data into persistent store coordinator
}
Each method I described contains various steps (non atomic operations, except the download one). So, a cancellation could happen within each of these step (in a not predefined manner). Could I check the isCancelled property before each step? Does this work?
Edit 2 based on Tammo Freese' edit
I understand what do you mean with your edit code. But the thing I'm worried is the following. A cancel request (the user can press the home button) can happen at any point within the main execution, so, if I simply return, the state of the operation would be corrupted. Do I need to clean its state before returning? What do you think?
The problem I described could happen when I use sync operations (operations that are performed in a sync fashion within the same thread they run). For example, if the main is downloading a file (the download is performed through +sendSynchronousRequest:returningResponse:error) and the app is put in background, what could it happen? How to manage such a situation?
// download
if ([self isCancelled])
return;
// downloading here <-- here the app is put in background
Obviously, I think that when the app is then put in foreground, the operation is run again since it has been cancelled. In other words, it is forced to not maintain its state. Am I wrong?
If I understand you correctly, you have a NSOperationQueue and if your application enters the background, you would like to
cancel all operations and
wait until the cancellations are processed.
Normally this should not take too much time, so it should be sufficient to do this:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[_queue cancelAllOperations];
[_queue waitUntilAllOperationsAreFinished];
}
The definition of "too much time" here is approximately five seconds: If you block -applicationDidEnterBackground: longer than that, your app will be terminated and purged from memory.
Let's say that finishing the cancelled operations takes longer than 5 seconds. Then you have to do the waiting in the background (see the comments for explanations):
- (void)applicationDidEnterBackground:(UIApplication *)application
{
bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
// If this block is called, our background time of normally 10 minutes
// is almost exceeded. That would mean one of the cancelled operations
// is not finished even 10 minutes after cancellation (!).
// This should not happen.
// What we do anyway is tell iOS that our background task has ended,
// as otherwise our app will be killed instead of suspended.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Normally this one is fast, so we do it outside the asynchronous block.
[_queue cancelAllOperations];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Wait until all the cancelled operations are finished.
[_queue waitUntilAllOperationsAreFinished];
// Dispatch to the main queue if bgTask is not atomic
dispatch_async(dispatch_get_main_queue(), ^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
});
}
So basically we tell iOS that we need some time to perform a task, and when the task is finished of the time runs out, we tell iOS that our task has ended.
Edit
To answer the question in your edit: To respond to cancellation, just check for cancellation whenever you can, and return from the -main method. A cancelled operation is not immediately finished, but it is finished when -main returns.
- (void)main
{
// 1- download
if ([self isCancelled]) return;
// 2- parse
// 2.1 read file location
// 2.2 load into memory
while (![self isCancelled] && [self hasNextLineToParse]) {
// ...
}
// 3- import
// 3.1 fetch core data request
if ([self isCancelled]) return;
// 3.2 if data is not present, insert it (or update)
// 3.3 save data into persistent store coordinator
}
If you do not check for the cancelled flag at all in -main, the operation will not react to cancellation, but run until it is finished.
Edit 2
If an operation gets cancelled, nothing happens to it except that the isCancelled flag is set to true. The code above in my original answer waits in the background until the operation has finished (either reacted to the cancellation or simply finished, assuming that it does not take 10 minutes to cancel it).
Of course, when reacting to isCancelled in our operation you have to make sure that you leave the operation in a non-corrupted state, for example, directly after downloading (just ignoring the data), or after writing all data.
You are right, if an operation is cancelled but still running when you switch back to the foreground, that operation will finish the download, and then (if you programmed it like that) react to cancel and basically throw away the downloaded data.
What you could do instead is to not cancel the operations, but wait for them to finish (assuming they take less than 10 minutes). To do that, just delete the line [_queue cancelAllOperations];.