Update: The Swift version of this tutorial is available here.
Since iOS7, Apple engineers introduced a new set of networking API called NSURLSession, in order to help with downloading/uploading content via HTTP or HTTPS protocols.
NSURLSession came with a lot of improved tasks which developers had to write a lot of code to handle. These tasks are:
- Manage downloads when the app is in a background state.
- Offers a download session configuration object to store data in a file and continue the download task even when the app crashes or get suspended.
- Provide capabilities to pause, resume, cancel, restart the download task.
- Provide a way to notify the app about the download progress via its custom delegate object using a set of protocol methods.
- Provide the ability to resume suspended, canceled or failed downloads where they left off.
In this tutorial, you will learn how to download a PDF file using NSURLSession. You will be experiencing the common tasks like pause, resume, cancel and restart the download along the way. After downloading the file, you will visualize it using UIDocumentInteractionController.
So, let’s get this done 🙂
For this tutorial, I will be using Xcode 6, but if you are using Xcode 5, that should be fine since the NSURLSession API classes doesn’t get changed in the iOS SDK8.
Open up Xcode, create a new project. Choose the Single View Application template and click next.
In the next screen, give your app a name (PDFDownloader for example) and make sure the chosen Language is Objective-C from the drop down list.
Click next and then choose a location where to save your project and click ‘Create’ button.
Before coding, let’s add the basic controls for your project. Select Main.storyboard from the project navigator where all your project files are listed in the left pane.
You may notice the view controller bounds are larger than what you use to see in Xcode 5 and earlier. Xcode 6 set this size frames by default, so you need to adjust them manually to the iPhone screen size before you continue.
To do that, select the view controller, and from the File Inspector, make sure ‘Use Size Classes’ is UNchecked.
From the prompt, make sure you select ‘iPhone’ from the drop down list in order to get the view controller an iPhone frames. Then click the ‘Disable Size Classes’ button.
You will notice the view controller frames are now the same as the iPhone frames. Now, you can comfortably continue your work 🙂
As I said, you will be downloading a PDF file from a HTTP source. So let’s add a button which will start the download process for you, and three other buttons to respectively pause, resume and cancel the download.
From the object library, drag a UIButton to the view and repeat the same step three more times to bring in three more buttons. Customize the look of the buttons the way you want.
Next, drag a UIProgressView from the Object Library to the view. The progress view will be useful to show you the progress of the download task as bytes are being downloaded.
Your view will look something like this:
Ok, now it’s time to code. You need to add action methods for the buttons and also you need to bind the UIProgressView to an outlet in order to control it from within your code.
Let’s define actions for the buttons. Select Main.storyboard and then switch to the ‘Assistant editor’.
Then, ctrl click on the ‘Download’ button and drag a line from the button to the ViewController.m which get shown on the right view after you switched to the Assistant editor.
Release the mouse click, a popup will show up to setup your method. Give the action method a name like ‘downloadFile’ and make sure the defined event is ‘Touch Up Inside’ like the image above, then click Connect.
Now repeat the same step with the remaining buttons. Name the buttons actions: pauseDownload, resumeDownload, cancelDownload respectively.
Last thing before you go for coding, you need to add the outlet connection for the UIProgressView. Ctrl click on the UIProgressView and drag a line in between the @interface and @end block in the ViewController.m file.
Name it progressView and click Connect. After connecting the actions and outlet, your file ViewController.m should look like this:
That’s all for the UI, now time for code 😉
Switch to the Standard editor and select the ViewController.m file.
Add an NSURLSessionDownloadTask variable between the curly brackets like this:
[sourcecode lang=”objc”]
@interface ViewController (){
NSURLSessionDownloadTask *download;
}[/sourcecode]
After the closing bracket, add a property for the background session you will be using to download the task.
[sourcecode lang=”objc”]
@property (nonatomic, strong)NSURLSession *backgroundSession;
[/sourcecode]
The @interface block in the ViewController.m file should look like this:
[sourcecode lang=”objc”]
#import "ViewController.h"
@interface ViewController (){
NSURLSessionDownloadTask *download;
}
@property (nonatomic, strong)NSURLSession *backgroundSession;
@property (strong, nonatomic) IBOutlet UIProgressView *progressView;
@end
[/sourcecode]
You will be running a download task for the sake of this tutorial, that’s why you declared an NSURLSessionDownloadTask object. And the backgroundSession property will be used to initialize the download object (NSURLSessionDownloadTask) in order to start the download task.
Move to viewDidLoad method and right after [super viewDidLoad], write the following code:
[sourcecode lang=”objc”]
// 1
NSURLSessionConfiguration *backgroundConfigurationObject = [NSURLSessionConfiguration backgroundSessionConfiguration:@"myBackgroundSessionIdentifier"];
// 2
self.backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfigurationObject delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// 3
[self.progressView setProgress:0 animated:NO];
[/sourcecode]
Let’s explain the above code:
1/ NSURLSession API has 3 types of sessions: default, ephemeral and download sessions. The behavior of a session is determined by the configuration object used to create it. In this tutorial, you are going to use a background configured session because the download task of the file should be done in the background thread.
2/ Then you create the NSURLSession object using the configuration object created above. The delegateQueue argument is a queue for scheduling the delegate calls and completion handlers which get called by the delegate object (self).
3/ Here you reset the progress property of progressView to 0 because at that time the download doesn’t start yet.
When the download task starts, the NSURLSessionDownloadTask object will throw events, for example, the object will notify the delegate about how many bytes are downloaded and how many bytes are left. The delegate should implement some protocol methods in order to take actions when these kind of events occurs.
In ViewController.m file, right before @end, paste the following code:
[sourcecode lang=”objc”]
// 1
– (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectoryPath = [paths objectAtIndex:0];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *destinationURL = [NSURL fileURLWithPath:[documentDirectoryPath stringByAppendingPathComponent:@"file.pdf"]];
NSError *error = nil;
if ([fileManager fileExistsAtPath:[destinationURL path]]){
[fileManager replaceItemAtURL:destinationURL withItemAtURL:destinationURL backupItemName:nil options:NSFileManagerItemReplacementUsingNewMetadataOnly resultingItemURL:nil error:&error];
[self showFile:[destinationURL path]];
}else{
if ([fileManager moveItemAtURL:location toURL:destinationURL error:&error]) {
[self showFile:[destinationURL path]];
}else{
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"PDFDownloader" message:[NSString stringWithFormat:@"An error has occurred when moving the file: %@",[error localizedDescription]] delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
[alert show];
}
}
}
// 2
– (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
[self.progressView setProgress:(double)totalBytesWritten/(double)totalBytesExpectedToWrite
animated:YES];
}
// 3
– (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"PDFDownloader" message:@"Download is resumed successfully" delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
[alert show];
}
[/sourcecode]
The above delegate methods are important to handle common events, let’s explain them quickly:
1/ didFinishDownloadingToURL delegate method provides the app with the URL to a temporary file where the downloaded content is stored, so you need to move your content to a permanent location. That’s what you did above, you tried to move your file to a path in the document directory in your device. If the path already exist, then you replace the file with the new path (if you don’t do that, an error will raise because the system cannot override a file automatically in case there is a path duplication).
2/ downloadTask:didWriteData delegate method provides the app with status informations about the progress of the download, you use this delegate method to increase the progress property of the progress view so that it always reflect how much time is left for the task to finish. You get the progress value by dividing the total bytes written by the total bytes expected to write.
3/ downloadTask:didResumeAtOffset delegate method is called when its attempt to resume a previously failed download was successful.
Next, you should implement one more delegate method which get called when the download task is completed. Paste the following method:
[sourcecode lang=”objc”]
– (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
download = nil;
[self.progressView setProgress:0];
if (error) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"PDFDownloader" message:[error localizedDescription] delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
[alert show];
}
}
[/sourcecode]
Nothing complicated above, since your download is completed, you need to set the download variable to nil because you don’t need it anymore, and reset the progress view to 0.
Note that URLSession:task:didCompleteWithError is an NSURLSessionTaskDelegate protocol method, this is a general protocol method which is useful for all types of tasks (download, upload, etc).
Ok, now that you managed all required and useful NSURLSession related protocol methods, you need to handle the case when you complete downloading the file. At that time, you will open the PDF file to visualize it, so let’s go ahead to implement the showFile method.
Paste the following code in ViewController.m file:
[sourcecode lang=”objc”]
– (void)showFile:(NSString*)path{
// Check if the file exists
BOOL isFound = [[NSFileManager defaultManager] fileExistsAtPath:path];
if (isFound) {
UIDocumentInteractionController *viewer = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];
viewer.delegate = self;
[viewer presentPreviewAnimated:YES];
}
}
[/sourcecode]
If the file exist at the given path, you open it using UIDocumentInteractionController. Pretty simple 🙂
In order to use presentPreviewAnimated method, you should implement the protocol method documentInteractionControllerViewControllerForPreview, so go ahead and add its implementation.
[sourcecode lang=”objc”]
– (UIViewController *) documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller{
return self;
}
[/sourcecode]
Now, you should implement the buttons action implementations, no worries, they are few lines of code and you should be set.
Always in the ViewController.m file, add the following code:
[sourcecode lang=”objc”]
– (IBAction)downloadFile:(id)sender {
if (nil == download){
NSURL *url = [NSURL URLWithString:@"http://www.nbb.be/DOC/BA/PDF7MB/2010/201000200051_1.PDF"];
download = [self.backgroundSession downloadTaskWithURL:url];
[download resume];
}
}
– (IBAction)pauseDownload:(id)sender {
if (nil != download){
[download suspend];
}
}
– (IBAction)resumeDownload:(id)sender {
if (nil != download){
[download resume];
}
}
– (IBAction)cancelDownload:(id)sender {
if (nil != download){
[download cancel];
}
}
[/sourcecode]
In downloadFile method, you used the backgroundSession property to initialize a new download session task. Then you call resume to start the task.
The rest of action methods are self explanatory, they shouldn’t be hard for you 😉
Now you are set and ready to run your program, but there is a small thing missing. You need to let your controller conform to the NSURLSessionDownloadDelegate and UIDocumentInteractionControllerDelegate protocols. For that, switch to ViewController.h and change it to look like the following:
[sourcecode lang=”objc”]
@interface ViewController : UIViewController <NSURLSessionDownloadDelegate, UIDocumentInteractionControllerDelegate>
@end
[/sourcecode]
That’s it, all is ready to go. Run your app and click the Download button, feel free to pause and resume the download.
After the download is finished, the PDF file will be presented in a modal controller.
As usual, you can download the running project here.
Hope you enjoyed this tutorial. More tutorials are in the queue for you 🙂
Fee free to leave a comment, I am looking forward to hearing from you!
Also, in response to your emails, I wrote a Swift 2 version of this tutorial for you here. You are welcome 🙂
nice tutorial.
but what if i don’t have the direct url to the pdf but instead i have like http://www.somesite.com/file.asp?file=123456
how can i download the file?
Hi Kupilot, if the url is publicly accessible, I think the format you suggested should work fine. Did you tried it already? If not, what do you get as an error ?
hi, i am getting message “lost connection to background transfer service” when app is suspended. using iOS 8.3
Hi Mohit, I am also testing with the 8.3 version, however I couldn’t reproduce the error you got. If you can give some steps to reproduce, that will help 🙂
For the upload task, you can easily use the uploadTaskWithRequest:fromData: API to upload the data as NSData, there is also two alternatives to upload data as File and Stream. This could be a subject for a new tutorial though 🙂
Official documentation has some sample codes to get you started https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html
Hi, I was using a similar code to download files in my app and it worked perfectly, but the server has changed to use CDN and the URLs of the files are dynamic now. When I paused the download and after I resumed with a different link the download was restarted. You have some idea how to fix this? Thanks.
Hi 🙂
I think it’s pretty obvious for the system to restart the download session, since the url is changing. Do you expect different behaviour? Let me know how you want your code to behave ?
Hi Malek_T.
I expected the AFNetworking did the checking of temporary file by the file name or some other way (matching md5sum, by example), not only by url, like some download managers can continues the download of a file even with the link change. Can’t the NSURLSession do this? should use I another resource of AFNetworking?
English isn’t my first language, so please excuse any mistakes.
Thanks.
Hi Murilo,
So far, NSURLsession cannot verify md5 sum by its own. However, you can use some third libraries, like this one: https://github.com/jameshuynh/ObjectiveCDM
Obviously, you still can use Objective-C libraries using a bridging header file.
Hi Malek_T.
I’ll see this project, looks good, thank you!
Hi I use your code in a table view but when I go back using the navigation controller, it will not show the file anymore.
Hi,
Did you finish the file download and open it but cannot find it anymore after navigating forward and back to the file screen ?
Can you elaborate with more details so that I can help you 🙂
Yup! That’s exactly the problem! Can you help me resolve this?
Sure, as I am not aware about your project structure, can you send me the project by email? I will see what I can do to help you out understanding the issue and resolving it.
Great tutorial, though it took some time for me to work it out in Swift. I just want to mention that I ran into problems because I was using a static string as the identifier for the NSURLSessionConfiguration. In my project, whenever I popped the viewController from the navigationController and then came back to it, viewDidLoad was called again and I was getting an error in the console that a session with the identifier already exists. I solved it by using a date stamp as the configuration identifier. Here is some code in Swift:
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = “yyyy-MM-dd-HH-mm-ss”
let now = NSDate()
let dateString = dateFormatter.stringFromDate(now)
var backgroundConfigurationObject = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(dateString)
self.backgroundSession = NSURLSession(configuration: backgroundConfigurationObject, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
Hi Ben 🙂
I am not sure why using the date stamp did the trick for you, as I embedded a UINavigationController and I didn’t encounter any thrown error. Maybe there is something else in your app that I am not aware of. Anyway, good that you use the backgroundSessionConfigurationWithIdentifier API as the backgroundSessionConfiguration method is now deprecated.
What is your email address?
it’s malek.isims88@gmail.com
I already shared my project to you. Thank you!
Hey,
When I press the Download, downloadFile method gets called. But [download resume] is not working.
– (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location is never invoked.
Can you help?
I found the issue.
The simulator was not connected to internet. And we had no check to see whether data connection is available.
It is working now.
Thanks :p
Hey,
I have tried to open a link of dropbox to download the project, but the link says There is no file available. So, can you please provide any other link to get the working project? Also, Does your provide support for FTP connection?
Hi 🙂
I have fixed the link, you can download the project here http://sweettutos.com/wp-content/uploads/2015/10/PDFDownloader.zip
Thanks Malek for fixing link. Now I am able to download the code.
Thanks once again. 🙂
Hi Can you please provide information on how to use DidReceiveChallenge in NSURLSession?
I need to alert user asking for password when there is an authChallenge. Only after the alert closes the completion block has to be called. How to do this ?