How to use restclient dropbox API with NSURLSessionDownloadTask to download files - ios

Problem: i want to download a file from my dropbox account and use quick look to visualize it.
First Solution:
1) use Dropbox API restClient:
[[self restClient] loadFile:fullpath intoPath:finalpath];
2) Once downloaded use QLPreviewController to preview the file.
The problem with this solution is that I don't know how to synchronize the download with the preview (to use quick look the file needs to be local, so I need to download it first).
The (ugly) workaround I came up with is to set up an alert ("Caching") and make it last an arbitrary length of time (let's say 12 sec, magic number...). At the same time I pause execution for 10-12 seconds (magic numbers):
[NSThread sleepForTimeInterval:12.0f];
...and hope at the end of this time interval the file is downloaded already so I can start the QLPreviewController.
Here is the code (ugly, I know....):
// Define Alert
UIAlertView *downloadAlert = [[UIAlertView alloc] initWithTitle:#"caching" message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:nil] ;
// If file does not exist alert downloading is ongoing
if(![[NSFileManager defaultManager] fileExistsAtPath:finalpath])
{
// Alert Popup
[downloadAlert show];
//[self performSelector:#selector(isExecuting) withObject:downloadAlert afterDelay:12];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Here your non-main thread.
if(![[NSFileManager defaultManager] fileExistsAtPath:finalpath])
{
[NSThread sleepForTimeInterval:12.0f];
}
dispatch_async(dispatch_get_main_queue(), ^{
// Dismiss alert
[downloadAlert dismissWithClickedButtonIndex: -1 animated:YES];
//Here we return to main thread.
// We use the QuickLook APIs directly to preview the document -
QLPreviewController *previewController = [[QLPreviewController alloc] init];
previewController.dataSource = self;
previewController.delegate = self;
// Push new viewcontroller, previewing the document
[[self navigationController] pushViewController:previewController animated:YES];
});
});
It does work (with small files and fast connection) but It's not the best solution... .
I think that the best solution would be integrate NSURLSession with dropbox restClient so to use this routine:
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDownloadTask *task;
task = [session downloadTaskWithRequest:request
completionHandler:^(NSURL *localfile, NSURLResponse *response, NSErr or *error) {
/* yes, can do UI things directly because this is called on the main queue */ }];
[task resume];
But I'm not sure how to use it with the DropBox API: any suggestion ?
Thanks,
dom

It looks like the API tells you about progress and completion:
- (void)restClient:(DBRestClient*)client loadedFile:(NSString*)destPath contentType:(NSString*)contentType metadata:(DBMetadata*)metadata;
- (void)restClient:(DBRestClient*)client loadProgress:(CGFloat)progress forFile:(NSString*)destPath;
No need to do any sleeping or gcd calls directly. Just change your UI to show busy when the download starts and use these to update UI with progress and completion.

Related

Suddenly iOS App is not responsive at all

After I call a certain Google's Youtube library, my application suddenly becomes not responsive at all after one of its callback.
Not responsive means all UI components cannot be clicked.
Is there such thing in iOS that can disable entire screen to be not responsive at all?
The code:
self.uploadFileTicket = [service executeQuery:query
completionHandler:^(GTLServiceTicket *ticket,
GTLYouTubeVideo *uploadedVideo,
NSError *error) {
// Callback
_uploadFileTicket = nil;
if (error == nil) {
[_delegate videoUploadedSuccessfully:YES error:nil];
} else {
[_delegate videoUploadedSuccessfully:NO error:error.description];
}
}];
Inside my ViewController:
- (void)videoUploadedSuccessfully:(BOOL)success error:(NSString *)errorMessage{
dispatch_async(dispatch_get_main_queue(), ^{
if(success){
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Youtube"
message:#"Video uploaded successfully"
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:#"OK", nil];
[alert show];
}
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Youtube"
message:errorMessage
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:#"OK", nil];
[alert show];
}
});
}
Update
I have tried using Instrument and got following:
Does this mean my Working Thread are blocking Main Thread?
Finally I found the ROOT cause of this issue. There is somewhere in the code before uploading the video to Youtube:
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
I cannot pin-point the issue. But, here are some of my suggestions.
The method call [service executeQuery:query completionHandler: has a completion handler. Therefore, it's mostly an async task. Therefore the task of the service should be done in a background thread and should not block the UI.
In case you're not sure whether the call is in the main thread, use the following LOC to clarify.
[NSThread isMainThread] will return true only if the executing thread is the main thread/ UI-thread.
Can you also put a NSLog at videoUploadedSuccessfully and check whether the delegate method gets called multiple times?
You do not need the block
dispatch_async(dispatch_get_main_queue(), ^{}
The delegate method should get executed on the main thread itself as long as you're calling the service-query method on the main thread.
Finally check whether you're calling the [service executeQuery:query method from the main thread?

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.

UI still delayed even using GCD

In my modal UI there is a "DONE" button linked with IBAction -done:, it will upload a text to (lets say Dropbox server). Its code looks like this
- (IBAction)done:(id)sender {
// must contain text in textview
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
if (![_textView.text isEqualToString:#""]) {
// check to see if we are adding a new note
if (!self.note) {
DBFile *newNote = [[DBFile alloc] init];
newNote.root = #"dropbox";
self.note = newNote;
}
_note.contents = _textView.text;
_note.path = _filename.text;
// - UPLOAD FILE TO DROPBOX - //
NSLog(#"Initializing URL...");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSURL *url = [Dropbox uploadURLForPath:self.note.path];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"PUT"];
NSData *noteContents = [self.note.contents dataUsingEncoding:NSUTF8StringEncoding];
NSLog(#"Creating session task...");
NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:request
fromData:noteContents
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *resp = (NSHTTPURLResponse *) response;
if (!error && resp.statusCode == 200) {
NSLog(#"OK");
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
});
} else {
NSLog(#"Status code: %d", resp.statusCode);
}
}];
[uploadTask resume];
});
} else {
UIAlertView *noTextAlert = [[UIAlertView alloc] initWithTitle:#"No text"
message:#"Need to enter text"
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[noTextAlert show];
}
}
The delegate method noteDetailsViewControllerDoneWithDetails: of this class is look like this
-(void)noteDetailsViewControllerDoneWithDetails:(NoteDetailsViewController *)controller{
// refresh to get latest
[self dismissViewControllerAnimated:YES completion:nil];
[self notesOnDropbox];}
(notesOnDropbox is a time-consuming task). When DONE button is tapped, I expect this modal VC/UI to dismiss immediately and it fetches data on background (by notesOnDropbox method). However, when I try tapping DONE button, my UI stop responding for about seconds, after that the modal UI is dismissed. I cannot figure out where I misuse the GCD. Please help me.
if you want to dismiss your modal VC/UI immediately, just ask the delegate to dismiss,
like is:
- (IBAction)done:(id)sender {
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
// ...
}
In your sample code,
you do the dismiss action after the upload task completed, but the upload task is asynchronous.
and you ask the delegate to dismiss use GCD dispatch_async, this is asynchronous task, too.
After all, you have to consider the what time to do upload, who to do upload task and what time to invoke notesOnDropbox.
First, if notesOnDropbox is a time-consuming task, then you should not be performing it on the main thread (as you are doing). If it is sufficiently time-consuming and you do it on the main thread, the WatchDog process will kill your app dead right before the user's eyes.
Second, there is no need to get off the main thread to do an upload. If you use NSURLSession correctly, it will be asynchronous.
Your code only calls noteDetailsViewControllerDoneWithDetails when the whole upload task is completed, because that's how you wrote your code. Actually, the situation seems worse. If the upload task has any kinds of problems, noteDetailsViewControllerDoneWithDetails will never be called.
You need to call noteDetailsViewControllerDoneWithDetails as soon as possible, and then think about what you are going to do when the upload fails - which might easily happen a long time later.

How to update a UILabel synchronously with code in a method using setNeedsDisplay

I am downloading a zip file from the internet and unzipping it. The process takes 15 seconds or so. I want to display some sort of time elapsed to the user. As a start I just want to update a UILabel on my _countUpView UIView when the download has finished and the unzipping begins. I believe that I need to use setNeedsDisplay on the view that holds the UILabel but I can not seem to figure out what I am doing wrong, here is my code:
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://URLtothiszipfile>/all.zip"]];
NSData *slicedata= [NSData dataWithContentsOfURL:url];
NSError *error;
NSString *isignsPath = [NSString stringWithFormat:#"%#/iSigns",docsDir];
if (![filemgr fileExistsAtPath:isignsPath]){ //Does directory already exist?
if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error:&error]){
NSLog(#"Create directory error: %#", error);
}
}
thePathAndName= [NSString stringWithFormat:#"%#/iSigns/all.zip", docsDir];
[slicedata writeToFile:thePathAndName atomically:YES];
_countUpLab.text= #"Unzipping";
[self displayCountUpNOW];
NSLog(#"Unzipping");
[SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:#"" error:&error];
if (error.code!= noErr){
NSLog(#"Unzip error");
return;
}else{
NSLog(#"Unzipped successfully");
}
And the setNeedsDisplay method is:
- (void) displayCountUpNOW {
dispatch_async(dispatch_get_main_queue(), ^{
[_countUpView setNeedsDisplay];
});
}
My label does not change to "Unzipping" until NSLog shows "Unzipped successfully", about 10 seconds after I set the label to "Unzipping".
In those 10 seconds, the unzipping occurs but a timer that I want to use to update the label every second stops executing too so I can't display the elapsed time in the label either.
Please help me understand this asynchronous environment.
Carmen
EDIT-EDIT-EDIT
The below code seems to work asynchronously and I even have my elapsed time indicator working since my timer isn't stopped. I couldn't find a method in SSZipArchive that works without a file so I left the save file in. How did I do Duncan? Thanks again, that is pretty slick!
ONE MORE QUESTION: What is the best way to know when an asynchronous request is still outstanding, by setting a global flag variable when the request is made and clearing it when the async process completes?
gotALL= 0;
_countUpLab.text= #"Downloading";
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://URLtothiszipfile>/all.zip"]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *slicedata, NSError *error1){
if ([slicedata length]> 0 && error1== nil){
NSString *isignsPath = [NSString stringWithFormat:#"%#/iSigns",docsDir];
if (![filemgr fileExistsAtPath:isignsPath]){ //Does directory already exist?
NSError *error2;
if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error: &error2]){
NSLog(#"Create directory error: %#", error2);
[self endCountUp];
return;
}
}
thePathAndName= [NSString stringWithFormat:#"%#/iSigns/all.zip", docsDir];
[slicedata writeToFile:thePathAndName atomically:YES];
_countUpLab.text= #"Unzipping";
NSLog(#"Unzipping");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error3;
[SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:#"" error:&error3];
dispatch_async(dispatch_get_main_queue(), ^(void) {
if (error3.code!= noErr){
NSLog(#"Unzip error %#", error3.description);
[self endCountUp];
return;
}else{
NSLog(#"Unzipped successfully");
gotALL= 1;
[self endCountUp];
return;
}
});
});
}else if ([slicedata length]== 0 && error1== nil){
//todo yet
}else if (error1!= nil){
//todo yet
}
}];
Lots of problems with your code. First of all, Your current code will lock up the user interface until the download is complete. That is bad, even for short downloads. If there is a network problem, this can lock up the user interface for up to 2 minutes, even for a short file.
You should download the file using an async download, not synchronous.
Second of all, UI updates only get rendered to the screen when your code returns and the app visits the event loop. Code using this approach:
Set label text to "doing stuff"
Do something time-consuming
Change label text to "done with stuff"
Does not work. The text "doing stuff" never shows up in the label, because the label changes aren't drawn until your code finishes and returns, and by then, you've replaced the label text with "done with stuff."
Here's what you should do instead:
Use the NSURLConnection method sendAsynchronousRequest:queue:completionHandler:. Put the code that you want to run once the download is complete in the completionHandler block.
That method handles the background stuff for you, and then runs your completion block on the main thread once the download is complete. It's a slick system method.
It should be possible to do the unzipping from the background using dispatch_async. (I'm not familiar with the SSZipArchive library, so I'm not positive that it's thread-safe, but it should be.)
The basic idea is:
Display a "downloading" message.
Create an NSURLRequest.
Use sendAsynchronousRequest:queue:completionHandler: to submit the request asynchronously.
In the completion handler:
Change the message to "unzipping"
Save the downloaded data to disk if the unzip library requires that it be saved to a file. If the zip library can do the unzipping from memory, you can skip this step.
Use dispatch_async to unzip the file from the default priority global queue
At the end of the unzip block, use dispatch_async(dispatch_get_main_queue()) to change the label to "unzip complete" or whatever you want to say. (You have to send the message to the main queue because you can't change the UI from a background thread.)
Try and figure out how to code the above approach. If you get stuck, post what you've tried as an edit to your post and we can guide you, but it's better if you try to write this code yourself. You'll learn that way rather than copy/pasting.

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

Resources