I've created a class called "ConnectionManager" that will handle all network request and fetch data from the server and after that call to a completion handler.
ConnectionManager.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UIAlertView+CustomAlertView.h"
#import <Crashlytics/Crashlytics.h>
#interface ConnectionManager : NSObject<NSURLSessionDataDelegate>
#property (weak, nonatomic) NSMutableData *receivedData;
#property (weak, nonatomic) NSURL *url;
#property (weak, nonatomic) NSURLRequest *uploadRequest;
#property (nonatomic, copy) void (^onCompletion)(NSData *data);
#property BOOL log;
-(void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *data))completionHandler;
-(void)uploadDataWithRequest:(NSURLRequest*)request withCompletionHandler:(void (^)(NSData *data))completionHandler;
#end
ConnectionManager.m
#import "ConnectionManager.h"
#implementation ConnectionManager
-(void)uploadDataWithRequest:(NSURLRequest*)request withCompletionHandler:(void (^)(NSData *data))completionHandler{
// Instantiate a session configuration object.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Configure Session Configuration
[configuration setAllowsCellularAccess:YES];
// Instantiate a session object.
NSURLSession *session=[NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
// Assign request for later call
self.uploadRequest = request;
// Create an upload task object to perform the data uploading.
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
// Assign completion handler
self.onCompletion = completionHandler;
// Inititate data
self.receivedData = [[NSMutableData alloc] init];
// Resume the task.
[task resume];
}
-(void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *data))completionHandler{
// Instantiate a session configuration object.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Configure Session Configuration
[configuration setAllowsCellularAccess:YES];
// Instantiate a session object.
NSURLSession *session=[NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
// Assign url for later call
self.url = url;
// Create a data task object to perform the data downloading.
NSURLSessionDataTask *task = [session dataTaskWithURL:self.url];
// Assign completion handler
self.onCompletion = completionHandler;
// Inititate data
self.receivedData = [[NSMutableData alloc] init];
// Resume the task.
[task resume];
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.receivedData appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
if (error.code == -1003 || error.code == -1009) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"Unable to connect to the server. Please check your internet connection and try again!" delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
if (buttonIndex==1) {
// Retry
if (self.url) {
NSURLSessionDataTask *retryTask = [session dataTaskWithURL:self.url];
[retryTask resume];
}else{
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
[task resume];
}
}else{
self.onCompletion(nil);
}
}];
}];
}else{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"An unkown error occured! Please try again later, thanks for your patience." delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert show];
CLS_LOG(#"Error details: %#",error);
self.onCompletion(nil);
}];
}
}
else {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.onCompletion(self.receivedData);
}];
}
}
#end
Here is the piece of code I use to call it :
-(void)loadDataFromServer{
NSString *URLString = [NSString stringWithFormat:#"%#get_people_number?access_token=%#", global.baseURL, global.accessToken];
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:URLString];
ConnectionManager *connectionManager = [[ConnectionManager alloc] init];
[connectionManager downloadDataFromURL:url withCompletionHandler:^(NSData *data) {
if (data != nil) {
// Convert the returned data into an array.
NSError *error;
NSDictionary *number = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (error != nil) {
CLS_LOG(#"Error: %#, Response:%#",error,[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
}
else{
[_mapView updateUIWithData:[number objectForKey:#"number"]];
}
}
}];
}
I figured out when using Instruments that All objects of ConnectionManager type are persistent even after getting the data from server and calling the completion handler.
I tried to change the completion handler property from copy to strong, but I got the same results. Changing it to weak cause a crash and it never be called.
Please someone guide me to the right way.
After doing a lot of research, I found that the URL Session object is still alive.
Based on Apple Documentation Life Cycle of URL Session there's two cases:
Using the system provided delegate (by not setting a delegate), here the system will take care of invalidating the NSURLSession object.
Using a custom delegate. When you no longer need a session, invalidate it by calling either invalidateAndCancel (to cancel outstanding tasks) or finishTasksAndInvalidate (to allow outstanding tasks to finish before invalidating the object).
To fix the code above I only changed the delegate method "didCompleteWithError" like the following:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
if (error.code == -1003 || error.code == -1009) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"Unable to connect to the server. Please check your internet connection and try again!" delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
if (buttonIndex==1) {
// Retry
if (self.url) {
NSURLSessionDataTask *retryTask = [session dataTaskWithURL:self.url];
[retryTask resume];
}else{
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
[task resume];
}
}else{
[session finishTasksAndInvalidate];
self.onCompletion(nil);
}
}];
}];
}else{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"An unkown error occured! Please try again later, thanks for your patience." delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert show];
[session finishTasksAndInvalidate];
self.onCompletion(nil);
}];
}
}
else {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[session finishTasksAndInvalidate];
self.onCompletion(self.receivedData);
}];
}
}
Related
I have used URLRequest to fetch a html file from a pc in my wifi network.
My app fetches a html file from my fileserver and the filename is a number which is typed in the app. Also I have given a 20 seconds timeout for the request. How can I detect whether timeout occurred because I have 2 situations,
1. file does not exist
2. connection is slow
In urlrequest, there is a BOOL for error,no description.
Suggest me a method if possible.
My code is below for only urlrequest
-(void)getHtmlContent{
[self.spinner startAnimating];
NSString *str = #"http://192.168.1.250/rec/";
// NSString *str = #"file:///Volumes/xampp/htdocs/";
str = [str stringByAppendingString:numEntered.text];
str = [str stringByAppendingString:#".html"];
NSURL *url=[NSURL URLWithString:str];
//self.htmlContent = nil;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//NSURLRequest *request = [NSURLRequest requestWithURL:url];
request.timeoutInterval = 20.0;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request completionHandler:^(NSURL *localfile, NSURLResponse *response, NSError *error) {
if(!error){
if([request.URL isEqual:url]){
NSString *content = [NSString stringWithContentsOfURL:localfile encoding: NSUTF8StringEncoding error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
[numEntered setText:#"0"];
text = #"";
[self.spinner stopAnimating];
self.htmlContent = content;
NSString *myHTMLString = self.htmlContent;
if(myHTMLString != nil) {
if([myHTMLString isEqualToString:#"3"]){
UIAlertView *alrt = [[UIAlertView alloc] initWithTitle:#"LogIn Success" message:#"" delegate:self cancelButtonTitle:#"Continue" otherButtonTitles:nil];
self.view.userInteractionEnabled = YES;
[alrt show];
}
else{
UIAlertView *alrt = [[UIAlertView alloc] initWithTitle:#"LogIn Failed. Try again." message:#"User not authenticated" delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil];
self.view.userInteractionEnabled = YES;
[alrt show];
}
}
});
}
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.spinner stopAnimating];
[numEntered setText:#"0"];
text = #"";
UIAlertView *alrt = [[UIAlertView alloc] initWithTitle:#"Requested file does not exist." message:#"Try again." delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil];
self.view.userInteractionEnabled = YES;
[alrt show];
});
}
}];
[task resume];
}
Since I can not comment I am writing as an Answer.
You need to check the error object to see the type of error that occured.
so in your else block you need to check error.localizedDescription to see what has happened, it would usually tell you that the file was not found or if the request timed out. you can even use your alert to show it like changing your else block as follows
else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.spinner stopAnimating];
[numEntered setText:#"0"];
text = #"";
UIAlertView *alrt = [[UIAlertView alloc] initWithTitle : #"Error"
message : error.localizedDescription
delegate : self
cancelButtonTitle : #"Ok"
otherButtonTitles : nil];
self.view.userInteractionEnabled = YES;
[alrt show];
});
}
You must use this delegate method to handle timeout:-
-(void) connection:(NSURLConnection * ) connection didFailWithError:(NSError *)error {
if (error.code == NSURLErrorTimedOut)
// handle error as you want
NSLog(#"Request time out");
}
I'm developing an application that should login to a remote service in the first view controller I create a UI to insert username and password.
When I press on the button login I make the following check:
I check if the field aren't empty with a simple if
From my button starts a segue to the internal view controller, before it shows me the internal view controller I added a method that should check if the user can login or not. In this method I call an external class in which I do the connection to the server to authenticate the user
The method to call the external class is the follow:
- (BOOL)loginSuccessWith:(NSString*)userName and:(NSString*)password {
ConnectionHandler *connectionHandler = [[ConnectionHandler alloc]init];
if ([connectionHandler startConnectionToServer:#"serverAddress" andUsername:userName withPassword:password andInstallationId:[[NSUserDefaults standardUserDefaults] objectForKey:#"instId"]]) {
return YES;
} else {
return NO;
}
}
As you can see if the method return YES or NO if the user can be logged or not.
In the ConnectionHandler class I wrote the following code:
#import "ConnectionHandler.h"
#interface ConnectionHandler() {
BOOL authenticated;
}
#end
#implementation ConnectionHandler
- (BOOL)startConnectionToServer:(NSString *)address andUsername:(NSString *)username withPassword:(NSString *)password andInstallationId:(NSString*) installationId {
if (![self sendRequestToURL:address withMethod:#"POST" withUsername:username withPassword:password andInstallationId: installationId]) {
NSLog(#"Impossibile connettersi");
return NO;
} else {
if (authenticated) {
return YES;
} else {
return NO;
}
}
}
- (id)sendRequestToURL:(NSString *)url withMethod:(NSString *)method withUsername:(NSString*)username withPassword:(NSString*)password andInstallationId:(NSString*)installationId {
NSURL *finalURL = [[NSURL alloc]init];
if ([method isEqualToString:#"POST"]) {
finalURL = [NSURL URLWithString:url];
} else {
NSLog(#"Metodo no previsto");
}
NSString *post = [NSString stringWithFormat:#"username=%#&password=%#&installationId=%#", username, password, installationId];
NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding];
NSString *postLength = [NSString stringWithFormat:#"%lu", (unsigned long)postData.length];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]init];
[request setURL:finalURL];
[request setHTTPMethod:method];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:postData];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
if (connection) {
[connection start];
}
return connection;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.responseData = [[NSMutableData alloc]init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Parsing della risposta dal server parlare con Giancarlo per vedere che tipo di risposta ottengo
NSDictionary *json;
NSError *err;
json = [NSJSONSerialization JSONObjectWithData:self.responseData options:NSJSONReadingMutableLeaves error:&err];
if (err) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"AT Brain" message:#"Impossibile satbilire una connessione con il server" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[alert show];
} else {
NSString *error_code = [NSString stringWithFormat:#"%#", [json objectForKey:#"error_code"]];
int success = [[json objectForKey:#"success"] intValue];
NSString *error_desc = [NSString stringWithFormat:#"%#", [json objectForKey:#"error_desc"]];
if ([self autenthicationOkWithErrorCode:error_code withSuccess:success andErrorDesc:error_desc]) {
authenticated = YES;
} else {
authenticated = NO;
}
}
}
- (BOOL)autenthicationOkWithErrorCode:(NSString*)error_code withSuccess:(int)success andErrorDesc:(NSString*)error_desc {
int errCode = [error_code intValue];
if (success == 1) {
return YES;
} else if (success == 0) {
if (errCode == 2) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"AT Brain" message:#"Controlla di aver inserito username, password e di avere un installationId" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[alert show];
return NO;
}
if (errCode == 3) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"AT Brain" message:#"Credenziali non valide, inserisci username e password corrette" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[alert show];
return NO;
}
if (errCode == 4) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"AT Brain" message:#"Utente non autorizzato ad accedere al servizio" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[alert show];
return NO;
}
if (errCode == 5) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"AT Brain" message:#"L'utenza a cui stai cercando di accedere è già associata ad un utente diverso" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[alert show];
return NO;
}
if (errCode == 6) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"AT Brain" message:#"Installation ID errato" delegate:nil cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[alert show];
return NO;
}
}
return NO;
}
I can connect to the server without problem, but before the - (void)connectionDidFinishLoading:(NSURLConnection *)connection is called it execute all the code in the - (BOOL)startConnectionToServer:(NSString *)address andUsername:(NSString *)username withPassword:(NSString *)password andInstallationId:(NSString*) installationId and it returns NO so the segue in the login view controller doesn't work because the method -(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender returns NO.
So my problem is how to wait the execution of the method - (void)connectionDidFinishLoading:(NSURLConnection *)connection is done before execute the else section in method - (BOOL)startConnectionToServer:(NSString *)address andUsername:(NSString *)username withPassword:(NSString *)password andInstallationId:(NSString*) installationId?
I hope you understand my issue and I hope you will help me to fix it, thank you
NSURLConnection is asynchronous. You kick it off and it immediately returns. You get callbacks (such as connectionDidFinishLoading) when it completes. That's the point at which you can check for success and move onto the next step.
I assume that loginSuccessWith:and: is called on the main thread (this is a very strange name for a method; you probably meant loginWithUsername:password:). So it can't block waiting for a network request that may take a very long time to complete. You'd hang the entire UI.
The URL Loading System Programming Guide has a great deal of information on how to design this. Look first at NSURLSession, and if it doesn't meet your needs, then use the lower-level NSURLConnection. With NSURLSession, you can pass completion blocks that will run whenever the operation completes.
I am creating a iPhone app which download icons using NSURLSession & NSURLDownloadTask.
In this case i want to download only the visible cell icons, when i scrolled the table all pending downloadTask get canceled (i.e download task for non-visible cells is must cancel). For this i created a method terminateAllDownloads().
// terminateAllDownloadTask
-(void)terminateAllDownloads
{
NSArray *allDownloads = [self.iconDownloadInProgress allValues];
[allDownloads makeObjectsPerformSelector:#selector(cancelDownload)];
[self.iconDownloadInProgress removeAllObjects];
}
// cancelDownload
-(void)cancelDownload
{
[self.downloadTask cancel];
[self.session invalidateAndCancel];
}
and call this function in dealloc & didRecieveMemoryWarning methods of MasterViewController.m
// dealloc
-(void)dealloc
{
[self terminateAllDownloads];
}
// didReceiveMemeoryWarning
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
[self terminateAllDownloads];
}
but this method is not called single time. how should i do this ?
I am using the NSURLSession Delegates, not completion block
//code
-(void)startDownload
{
appDelgate = [[UIApplication sharedApplication] delegate];
self.fileName = self.appData.name;
self.iconURL = self.appData.iconURL;
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.allowsCellularAccess = NO;
_session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
self.downloadTask = [_session downloadTaskWithURL:[NSURL URLWithString:_iconURL]];
[_downloadTask resume];
}
-(void)cancelDownload
{
[self.downloadTask cancel];
[self.session invalidateAndCancel];
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSString *trimmedString = [_fileName stringByReplacingOccurrencesOfString:#" " withString:#""];
NSString *appIconDirectory = [[documentsDirectoryForAppIcons absoluteString] stringByAppendingPathComponent:#"appIcons"];
NSURL* destinationUrlForAppIcon = [[NSURL URLWithString:appIconDirectory] URLByAppendingPathComponent:[NSString stringWithFormat:#"%#%#",trimmedString, #".png"]];
NSError *error1;
if([appIconFileManager fileExistsAtPath:[destinationUrlForAppIcon absoluteString]])
{
[appIconFileManager removeItemAtPath:[destinationUrlForAppIcon absoluteString] error:NULL];
}
BOOL status = [appIconFileManager copyItemAtURL:location toURL:destinationUrlForAppIcon error:&error1];
if (status && !error1)
{
[appDelgate.downloadedIcons setValue:destinationUrlForAppIcon.path forKey:self.iconURL];
if(self.completionHandler)
{
self.completionHandler(destinationUrlForAppIcon);
}
}
else
{
NSLog(#"File copy failed: %#", [error1 localizedDescription]);
}
}
Probably you refer to self in the block passed to the NSURLSession producing a retain cycle.
I am using NSURLSession for background image uploading. And according to uploaded image my server gives me response and I do change in my app accordingly. But I can't get my server response when my app uploading image in background because there is no completion block.
Is there way to get response without using completion block in NSURLUploadTask?
Here is my code :
self.uploadTask = [self.session uploadTaskWithRequest:request fromData:body completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSString *returnString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"returnString : %#",returnString);
NSLog(#"error : %#",error);
}];
[self.uploadTask resume];
But i got this error..
Terminating app due to uncaught exception 'NSGenericException', reason: 'Completion handler blocks are not supported in background sessions. Use a delegate instead.'
But if I can't use completion handler than how should I get the server response. It says use delegate but I can't find any delegate method which can gives me server response.
A couple of thoughts:
First, instantiate your session with a delegate, because background sessions must have a delegate:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kSessionIdentifier];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
Second, instantiate your NSURLSessionUploadTask without a completion handler, because tasks added to a background session cannot use completion blocks. Also note, I'm using a file URL rather than a NSData:
NSURLSessionTask *task = [self.session uploadTaskWithRequest:request fromFile:fileURL];
[task resume];
Third, implement the relevant delegate methods. At a minimum, that might look like:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSMutableData *responseData = self.responsesData[#(dataTask.taskIdentifier)];
if (!responseData) {
responseData = [NSMutableData dataWithData:data];
self.responsesData[#(dataTask.taskIdentifier)] = responseData;
} else {
[responseData appendData:data];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
NSLog(#"%# failed: %#", task.originalRequest.URL, error);
}
NSMutableData *responseData = self.responsesData[#(task.taskIdentifier)];
if (responseData) {
// my response is JSON; I don't know what yours is, though this handles both
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
if (response) {
NSLog(#"response = %#", response);
} else {
NSLog(#"responseData = %#", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);
}
[self.responsesData removeObjectForKey:#(task.taskIdentifier)];
} else {
NSLog(#"responseData is nil");
}
}
Note, the above is taking advantage of a previously instantiated NSMutableDictionary called responsesData (because, much to my chagrin, these "task" delegate methods are done at the "session" level).
Finally, you want to make sure to define a property to store the completionHandler provided by handleEventsForBackgroundURLSession:
#property (nonatomic, copy) void (^backgroundSessionCompletionHandler)(void);
And obviously, have your app delegate respond to handleEventsForBackgroundURLSession, saving the completionHandler, which will be used below in the URLSessionDidFinishEventsForBackgroundURLSession method.
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
// This instantiates the `NSURLSession` and saves the completionHandler.
// I happen to be doing this in my session manager, but you can do this any
// way you want.
[SessionManager sharedManager].backgroundSessionCompletionHandler = completionHandler;
}
And then make sure your NSURLSessionDelegate calls this handler on the main thread when the background session is done:
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
if (self.backgroundSessionCompletionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.backgroundSessionCompletionHandler();
self.backgroundSessionCompletionHandler = nil;
});
}
}
This is only called if some of the uploads finished in the background.
There are a few moving parts, as you can see, but that's basically what's entailed.
I'm using 'AFNetworking', '2.0.0' for download data.
I need to download large file. When user lock screen or press home button it should go pause (or continue downloading in background) and if I return to app it should resume.
Also I need to show progress of downloading.
I search a lot of examples but don't find anything 'AFNetworking', '2.0.0'.
I create app for iOS version 6.0+, so I can't use AFHTTPSessionManager or AFURLSessionManager.
For downloading in background on iOS 7 or higher i'am using NSURLSession with their NSURLSessionDownloadTask.
Switch on BackgroundMode in ProjectNavigator->YourProject->YourTarget->Capabilities (tab)->Background Modes
Add to your AppDelegate method - (BOOL)application:(UIApplication* )application didFinishLaunchingWithOptions:(NSDictionary* )launchOptions an initialiser for NSURLSessin with next code:
NSURLSessionConfiguration *sessionConfiguration;
NSString *someUniqieIdentifierForSession = #"com.etbook.background.DownloadManager";
if ([[[[[UIDevice currentDevice] systemVersion] componentsSeparatedByString:#"."] firstObject] integerValue] >= 8) {
//This code for iOS 8 and greater
sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:someUniqieIdentifierForSession];
} else {
this code for iOS 7
sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:someUniqieIdentifierForSession];
}
sessionConfiguration.HTTPMaximumConnectionsPerHost = 5;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
Of course, don't forget to declare session property with:
#property (nonatomic, strong) NSURLSession session;
Also add completion handler property (it will be needed if you want to get callback of background download process after Application termination or crash):
#property (nonatomic, copy) void(^backgroundTransferCompletionHandler)();
Add delegate methods in AppDelegate:
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *destinationFilename = downloadTask.originalRequest.URL.lastPathComponent;
NSURL *destinationURL = [[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:destinationFilename];
if ([fileManager fileExistsAtPath:[destinationURL path]]) {
[fileManager removeItemAtURL:destinationURL error:nil];
}
[fileManager copyItemAtURL:location
toURL:destinationURL
error:&error];
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
if (error != nil) {
NSLog(#"Download completed with error: %#", [error localizedDescription]);
}
else{
NSLog(#"Download finished successfully.");
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
if (totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown) {
NSLog(#"Unknown transfer size");
}
else{
NSLog(#"progress = %lld Mb of %lld Mb", totalBytesWritten/1024/1024, totalBytesExpectedToWrite/1024/1024);
}
}
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
ETBAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
// Check if all download tasks have been finished.
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([downloadTasks count] == 0) {
if (appDelegate.backgroundTransferCompletionHandler != nil) {
// Copy locally the completion handler.
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
// Make nil the backgroundTransferCompletionHandler.
appDelegate.backgroundTransferCompletionHandler = nil;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// Call the completion handler to tell the system that there are no other background transfers.
completionHandler();
// Show a local notification when all downloads are over.
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertBody = #"All files have been downloaded!";
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
}];
}
}
}];
}
//Background download support (THIS IMPORTANT METHOD APPLICABLE ONLY IN YOUR AppDelegate.m FILE!!!)
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
self.backgroundTransferCompletionHandler = completionHandler;
}
And declare that your class (in AppDelegate.h file for my example) conforms to protocol NSURLSessionDelegate like here:
#interface AppDelegate : UIResponder <UIApplicationDelegate, NSURLSessionDelegate>
Then add download task somewhere with:
NSURLSessionDownloadTask *task = [self.session downloadTaskWithURL:[NSURL URLWithString:urlString]];
task.taskDescription = [NSString stringWithFormat:#"Downloading file %#", [urlString lastPathComponent]];
[task resume];
So when your application started after termination, your session will be restored and delegate methods will be fired. Also if your application will waked up from background to the foreground your application delegate methods will be fired too.
when your app goes to background pause/stop the downloading operation and when comes in foreground resume your paused downloading operation in NSOperation queue.
Read these lines of code may help you:
AFDownloadRequestOperation *operation = [[AFDownloadRequestOperation alloc] initWithRequest:request targetPath:filePath shouldResume:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//handel completion
[operations removeObjectForKey:audioItem.filename];
}failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//handel failure
operation = [operations objectForKey:audioItem.filename];
[operation cancel];
[operations removeObjectForKey:audioItem.filename];
if (error.code != NSURLErrorCancelled) {
[self showErrorAlert];
NSLog(#"Error: %#", error);
}
}];
[operation setProgressiveDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) {
//handel progress
NSNumber *progress = [NSNumber numberWithFloat:((float)totalBytesReadForFile / (float)totalBytesExpectedToReadForFile)];
float progress=progress.floatValue;
[self performSelectorOnMainThread:#selector(updateProgressBar:) withObject:[NSArray arrayWithObjects:audioItem, progress, nil] waitUntilDone: YES];
}];
[operation setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
// TODO: Clean up operations
NSLog(#"App has been in background too long, need to clean up any connections!");
}];
[operations setValue:operation forKey:#"key"]; // store the NSOperation for further use in application to track its status.
[downloadQueue addOperation:operation];//here downloadQueue is NSOperationQueue instance that manages NSOperations.
To Pause downloading operation use this:
[operation pause];
To resume use this sort of line:
[operation resume];
Perhaps you're better off using DTDownload:
https://github.com/Cocoanetics/DTDownload
As far as I can tell, it doesn't have anything to report progress.
Edit: there's a nice tutorial over at AppCoda, about using NSURLSessionTask:
http://www.appcoda.com/background-transfer-service-ios7/