This tutorial will show you how to use the background transfer service, a Multitasking API provided by iOS 7. I’ll teach you how to create an app that will download a file without the application in the foreground. Once the file fully downloads, a notification message will pop-up. Continue reading to create this service!
Introduction
Background transfer service originated with iOS 6. This feature allowed apps to transfer files in both foreground and background modes, but limited the minutes. The biggest problem was when the “limited minutes” did not allow the user to download or upload large files. This is why Apple improved the framework in iOS 7.
With iOS 7, this feature underwent major changes, including:
- iOS manages downloads and uploads
- The transfer continues even when the user closes the application
- Time is unlimited
- It can be put in the queue anytime (foreground and background)
- The app wakes up to handle authentication, errors, or completion
- The app includes a Progress View
Background Transfer Service can be used for several distinct and useful tasks such as: uploading photos or videos, combining background fetch and remote notifications, and for keeping the app up to date, like with purchases for books, TV shows, podcasts, game content, maps, and more.
1. Setup the Project
To create this service, we need a single view with the following properties:
- A ViewController
- A NavigationController
- A Bar Item (to start the Download)
- An
UIDocumentInterationController
(to open the PDF document download)
First, start a new Xcode iPhone project. Then create a Single View Application. Next, go to the Main.Storyboard
and add some objects to our View. To add the NavigationController select the Default View Controller. In the Xcode menu, select Editor > Embed In > Navigation Controller. You need to drag-and-drop the Bar Item and the Progress View to your View Controller. Once you’re finished the View Controller should look similar to the following image:
Now, let’s add the properties necessary to interact with the objects we added. In ViewController.h
, add the following lines:
@property (weak, nonatomic) IBOutlet UIProgressView *progressView; - (IBAction)start:(id)sender;
Now change the view for the ViewController.m
. A warning will appear, but don’t worry about it; we’ll fix it later. Go back to the Main.Storyboard
and connect the objects with the properties and actions.
This step is trivial, but if you have any concerns feel free to use the comment section below.
2. NSURLSession
The NSURLSession
class and related classes provide an API to download or upload content via HTTP. This API is responsible for managing a set of transfer tasks. You will need to create three objects that directly relate with that class: one NSURLSession
, NSURLSessionDownloadTask
, and UIDocumentInteractionController
.
Your ViewController.h
will be something like this:
@property (nonatomic) NSURLSession *session; @property (nonatomic) NSURLSessionDownloadTask *downloadTask; @property (strong, nonatomic) UIDocumentInteractionController *documentInteractionController;
Additionally, you will also declare four protocols: NSURLSessionDelegate
, NSURLSessionTaskDelegate
, NSURLSessionDownloadDelegate
, and UIDocumentInteractionControllerDelegate
. Your @interface
should look like:
@interface ViewController : UIViewController < NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate,UIDocumentInteractionControllerDelegate>
The properties we’ll add are useful to instantiate and manipulate our session and the download process, which will allow you to resume, suspend, cancel, or retrieve the state. The last property, UIDocumentInterationController
is used to present the PDF document downloaded in this tutorial.
Now move to ViewController.m
.
The first task to complete is to add a string to the location of the file to download. You should use a standard Apple PDF.
static NSString *DownloadURLString = @"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/ObjC_classic/FoundationObjC.pdf";
On the viewDidLoad
method, let’s instantiate and set both the session and the progress views:
self.session = [self backgroundSession]; self.progressView.progress = 0; self.progressView.hidden = YES;
Since you call the backgroundSession
method, and it does not exist, you must declare it now. This method is responsible for dispatching one action that will be the background session. The backgroundSession
through the dispatch_once
executes a block once for the entire lifetime of the application. The NSString received by the NSURLSessionConfiguration
represents the ID of our session. This ID needs to be unique for each NSURLSession instance.
The complete method is as follows:
- (NSURLSession *)backgroundSession { static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"]; session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; }); return session; }
The (IBAction)start:(id)sender
method starts the document download. You will then initiate an NSURL
and a NSURLRequest
and use the downloadTask
property to pass the request object to the downloadTaskWithRequest
method. The complete method is below.
- (IBAction)start:(id)sender { if (self.downloadTask) { return; } NSURL *downloadURL = [NSURL URLWithString:DownloadURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; self.downloadTask = [self.session downloadTaskWithRequest:request]; [self.downloadTask resume]; self.progressView.hidden = NO; }
3. Protocols
At this point, you’ll notice that three warnings are present. They state that the protocol method should be implemented. The NSURLSessionDownloadDelegate
protocol defines the methods to handle the download task. To perform the download, it’s required to use the three delegate methods.
So, add the following three methods:
- 1.
(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
- 2.
(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL {
- 3.
(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
The (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
method is responsible to track the overall download process. It also updates the progressView
accordingly. The complete method is below.
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { if (downloadTask == self.downloadTask) { double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; NSLog(@"DownloadTask: %@ progress: %lf", downloadTask, progress); dispatch_async(dispatch_get_main_queue(), ^{ self.progressView.progress = progress; }); } }
4. Download Task
The (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL
method deals with the data itself (origin and destination). It controls the file only once it’s completely downloaded. To put it simply, it tells the delegate that a download task has finished downloading. It contains the session task that’s finished, the download task that’s finished, and a file URL where the temporary file can be found. It should look like this:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL { NSFileManager *fileManager = [NSFileManager defaultManager]; NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsDirectory = [URLs objectAtIndex:0]; NSURL *originalURL = [[downloadTask originalRequest] URL]; NSURL *destinationURL = [documentsDirectory URLByAppendingPathComponent:[originalURL lastPathComponent]]; NSError *errorCopy; // For the purposes of testing, remove any esisting file at the destination. [fileManager removeItemAtURL:destinationURL error:NULL]; BOOL success = [fileManager copyItemAtURL:downloadURL toURL:destinationURL error:&errorCopy]; if (success) { dispatch_async(dispatch_get_main_queue(), ^{ //download finished - open the pdf self.documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:destinationURL]; // Configure Document Interaction Controller [self.documentInteractionController setDelegate:self]; // Preview PDF [self.documentInteractionController presentPreviewAnimated:YES]; self.progressView.hidden = YES; }); } else { NSLog(@"Error during the copy: %@", [errorCopy localizedDescription]); } }
Finally, the (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
must also be declared. But be aware that we wont use it any further.
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { }
5. Session Tasks
You’re almost done with this class, only two methods remain: (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
, and - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
.
The first method informs the delegate that the task has finished transferring data. You should also use it to track any error that occurs.
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error == nil) { NSLog(@"Task: %@ completed successfully", task); } else { NSLog(@"Task: %@ completed with error: %@", task, [error localizedDescription]); } double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive; dispatch_async(dispatch_get_main_queue(), ^{ self.progressView.progress = progress; }); self.downloadTask = nil; }
Finally, you need to add the (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
method. It tells the delegate that all messages enqueued for a session have been delivered. It instantiates your AppDelegate
in order to launch a UILocalNotification
. The method you need to use is:
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; if (appDelegate.backgroundSessionCompletionHandler) { void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler; appDelegate.backgroundSessionCompletionHandler = nil; completionHandler(); } NSLog(@"All tasks are finished"); }
Several errors will appear because you haven’t imported the AppDelegate.h into your class yet.
#import "AppDelegate.h"
Move to the AppDelegate.h
and add the two following objects:
@property (strong, nonatomic) UIWindow *window; @property (copy) void (^backgroundSessionCompletionHandler)();
In the AppDelegate.m
you should implement the delegate method (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
. It tells the delegate that events related to a URL session are waiting to be processed and calls a custom method (presentNotification
) to notify the user when the file completely downloads. The complete method is:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { self.backgroundSessionCompletionHandler = completionHandler; //add notification [self presentNotification]; }
6. Local Notification
The presentNotification
method uses the UILocalNotification
class to create a local notification. It creates a sound and uses the badge system for that notification. Here’s the complete method:
-(void)presentNotification{ UILocalNotification* localNotification = [[UILocalNotification alloc] init]; localNotification.alertBody = @"Download Complete!"; localNotification.alertAction = @"Background Transfer Download!"; //On sound localNotification.soundName = UILocalNotificationDefaultSoundName; //increase the badge number of application plus 1 localNotification.applicationIconBadgeNumber = [[UIApplication sharedApplication] applicationIconBadgeNumber] + 1; [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification]; }
By the way, to restart the counter on the top left at the App Icon, you must add the following line to the (void)applicationDidBecomeActive:(UIApplication *)application
method in the AppDelegate
.
application.applicationIconBadgeNumber = 0;
The notification should be similar to the following image:
It is now time to Run
the app and test the background download.
Conclusion
At the end of this tutorial, you should have completed your background transfer service in iOS 7. You should understand the background transfer service and how to implement it. If you have any questions, please leave them in the comment section below.