ref – http://www.techotopia.com/index.php/An_iOS_7_Background_Transfer_Service_Tutorial
Background Uploads and Downloads
Adding support for background uploads and downloads is surprisingly easy with NSURLSession. Apple refers to them as out-of-process uploads and downloads as the tasks are managed by a background daemon, not your application. Even if your application crashes during an upload or download task, the task continues in the background.
Enabling background uploads and downloads is nothing more than flipping a switch in your session’s configuration. With a properly configured session object, you are ready to schedule upload and download tasks in the background.
When an upload or download is initiated:
- background daemon comes into existence
- The daemon takes care of the task and sends updates to the application through the delegate protocols declared in the NSURLSession API
- If app stops, daemon continues task
- If download task finishes, daemon informs session.
- The session then invokes the appropriate delegate methods to make sure your application can take the appropriate actions, such as moving the file to a more permanent location.
When the Background Transfer Service gets in action, what is actually happening is that the operating system takes charge of all the download process, performing everything in background threads (daemons).
While a download is in progress, delegates are used to inform the app for the progress, and wakes it up in the background to get more data if needed, such as credentials for logging in to a service. However, even though everything is controlled by the system, users can cancel all downloads at any time through the application.
A reminder that a block parameter has 3 parts. The return, the parameter, and the block name. Like so:
1 2 3 4 |
- (void)doSomethingWithBlock:(void (^)(double, double))block { ... block(21.0, 2.0); } |
So this means that the block is used where it takes 2 doubles, and does not return anything. It is called by using the block name ‘block’.
Handle Events for Background Session
1 2 3 |
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler |
Apps that (using an NSURLSession with a background configuration) may be launched or resumed in the background in order to handle the
completion of tasks in that session, or to handle authentication.
In other words, when an app uses NSURLSession with background service (whether its a GET request or downloading/uploading), NSURLSession needs to let the app know and to handle the completion of tasks for that session.
We simply strong the completionHandler block in order to call it later to let the system know that we complete all tasks.
NSURLSessionDelegate’s PROTOCOL METHOD will be called. In there you access the appDelegate and call the completionHandler.
Hence when you’re using it, 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.
Declare the session completion handler member variable:
AppDelegate.h
1 2 3 4 5 |
@interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; //custom block @property (copy) void (^sessionCompletionHandler)(); @end |
Then, have its set property copy the completion handler:
AppDelegate.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@implementation AppDelegate //add the delegate method so that the completion handler reference is stored: //when download is complete, it will notify the app through this delegate method - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { NSLog(@"AppDelegate.m - handleEventsForBackgroundURLSession: - daemon finished download. Notifies app through this protocol method"); NSLog(@"AppDelegate.m - handleEventsForBackgroundURLSession: - We strong the completionHandler"); self.sessionCompletionHandler = completionHandler; //(void(^)())completionhandler //completionHandler is the blockName // void is the return type // (^) means nullability // () means return type } |
Go to your ViewController and conform to NSURLSessionDelegate.
1 |
@interface ViewController () <NSURLSessionDelegate> |
This is so that we need to implement URLSessionDidFinishEventsForBackgroundURLSession.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { NSLog(@"ViewController.m - URLSessionDidFinishEventsForBackgroundURLSession"); AppDelegate * appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; if (appDelegate.sessionCompletionHandler) { NSLog(@"ViewController.m - local var block created and stronged appDelegate's sessionCompletionHandler"); void (^completionHandler)() = appDelegate.sessionCompletionHandler; NSLog(@"ViewController.m - appDelegate's sessionCompletionHandler block niled"); appDelegate.sessionCompletionHandler = nil; NSLog(@"ViewController.m - completionHandler called"); completionHandler(); } NSLog(@"URLSessionDidFinishEventsForBackgroundURLSession - Task complete"); } |
Now when your session object’s delegate queue tasks have all been processed, in your AppDelegate, you will receive a
application has received an -application:handleEventsForBackgroundURLSession:completionHandler:
message and your protocol method
1 2 3 |
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler |
will be called.
Then your session delegate will send a message to protocol method URLSessionDidFinishEventsForBackgroundURLSession to indicate that all messages previously enqueued for this session have been
delivered.
Hence that’s why its the job of your URLSessionDidFinishEventsForBackgroundURLSession method to simply call the completionHandler and clean it up.
If your session delegate sends a message to call protocol method:
1 2 |
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler; |
for authentication purposes, then you need to something similar. Where you give authentication information, then call the completionHandler().
Prepare the UI
1 2 3 4 5 6 7 8 9 10 |
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 480.0f)]; [_imageView setBackgroundColor:[UIColor yellowColor]]; [self.view addSubview:_imageView]; ... ... ... |
Configuration Object
1 2 3 4 5 6 7 8 9 10 11 |
NSLog(@"viewDidLoad - creating a new NSURLSessionConfiguration instance"); NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.ebookfrenzy.transfer"]; NSLog(@"viewDidLoad - specified a unique identifier"); NSLog(@"viewDidLoad - allowsCellularAccess set to YES to allow the transfer to use the cellular connection on the device if WiFi is not available"); configuration.allowsCellularAccess = YES; //For large transfers, this should be set to NO, or the user given //the opportunity to configure this setting to avoid incurring additional //costs on their cellular data plan. |
Session Object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (void)viewDidLoad { [super viewDidLoad]; //UI stuff //Configuration Object NSLog(@"viewDidLoad - session object sets configuration object"); _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; NSURL * downloadURL = [NSURL URLWithString:DownloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; NSLog(@"viewDidLoad - session calls downloadTaskWithRequest...! Make sure your protocol methods are implemented..! "); _downloadTask = [self.session downloadTaskWithRequest:request]; //default request [_downloadTask resume]; } |
Updating download progress percentages
1 2 3 4 5 6 7 8 |
//Sent periodically to notify the delegate of download progress - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { float progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; NSLog(@"URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite - progress is: %f", progress); } |
When you resume a download task
1 2 3 4 5 6 7 8 9 |
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { NSLog(@"URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes: %lld %lld", fileOffset, expectedTotalBytes); } |
Send last message related to a specific task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error == nil) { NSLog(@"URLSession:task:didCompleteWithError: No Error detected, Task %@ completed successfully", task); } else { NSLog(@"URLSession:task:didCompleteWithError: Task %@ completed with error: %@", task, [error localizedDescription]); } _downloadTask = nil; } |
When your download have completed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//Sent when a download task that has completed a download. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL { NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: Copying image file"); NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - init the file manager"); NSFileManager *fileManager = [NSFileManager defaultManager]; NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsDirectory = [URLs objectAtIndex:0]; NSURL *fromURL = [[downloadTask originalRequest] URL]; NSURL *destinationURL = [documentsDirectory URLByAppendingPathComponent:[fromURL lastPathComponent]]; NSError *error; NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - remove previous file if it exists"); // Remove file at the destination if it already exists. [fileManager removeItemAtURL:destinationURL error:NULL]; BOOL success = [fileManager copyItemAtURL:downloadURL toURL:destinationURL error:&error]; if (success) { NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - successfully, copied the file to local. Now displaying the image..."); UIImage *image = [UIImage imageWithContentsOfFile:[destinationURL path]]; _imageView.image = image; } else { NSLog(@"URLSession:downloadTask:didFinishDownloadingToURL: - File copy failed: %@", [error localizedDescription]); } } |