iDev:iOS 7 – NSURLSession

iOS 7 – NSURLSession

by Nicholas Cipollina 

NSURLConnection is the workhorse of networking code in the iOS 6 SDK, and virtually every app in the App Store uses it. NSURLConnection is both a class and a suite of related classes whose primary purpose is to either upload or download data to the web or even to the file system. In iOS 7, the networking stack has been overhauled to support NSURLSession, a completely rewritten and improved replacement API for NSURLConnection. NSURLConnection won’t be going away and is still supported; however, NSURLSession supports new, convenient behaviors that enable developers to easily support new iOS 7 features like background network requests. For this blog post I’m going to be discuss a sample app that downloads an image from the Internet, either in-process or in the background. If you’d like to skip right to it, the sample code it can be found on GitHub.

For Reference: Downloading An Image Using NSURLConnection

For comparison’s sake, the snippet below is a short implementation that represents how an iOS 6 application would use NSURLConnection.

NSURL *imageURL = [NSURL URLWithString:
@" http://www.nasa.gov/sites/default/files/ladee_9.4.13_nasa_edge_0.jpg";

NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
// Configure your request here.  Set timeout values, HTTP Verb, etc.
NSURLConnection *connection = 
         [NSURLConnection connectionWithRequest:request delegate:myConnectionDelegateClass];

This is a perfectly acceptable way to download data from the web, however NSURLSession will improve the behavior of requests similar to the one in this example.

What is NSURLSession?

NSURLSession is a replacement for NSURLConnection, and similarly it is both a distinct class and also a group of related APIs. It has is able to do everything NSURLConnection could do but also adds improvements on top. You may be asking yourself “if it is very similar to NSURLConnection, why replace it?” There are several reasons for this.

Instead of storing all of the networking objects (such as a response cache) globally, NSURLSession provides a mechanism for storing objects either on a global basis or on a per session basis. NSURLSession also provides better authentication handling, which is now done with an explicit authentication challenge on a connection basis instead of NSURLConnection’s request basis. NSURLSession provides a configurable container that applies to the session as opposed to configuration based solely on request. Probably the biggest improvement over NSURLConnection is NSURLSession’s support for out-of-process background transfers. This means that you can start a download of a large image or file, close the app and the download will continue until it completes. When the user re-launches the app, the app will be in the post-download state. This is possible because uploads and downloads are now done through the file system. This is beneficial because it optimizes battery life, uses the same delegate model as in-process transfers and supports UIKit multitasking.

The Classes of NSURLSession

NSURLSessionConfiguration is the first class you’ll probably work with in the NSURLSession API. It is a class that allows you to do exactly what its name suggests: configure your NSURLSession with connection/HTTP policies, caching behavior, maximum number of connections, custom headers, etc. For example, if you have a scenario where an authentication token is required to be passed in header on every call to a RESTful API, you can set the token in the session configuration and it will apply to all requests made using that session. With NSURLConnection, the token must be set on each and every request.

NSURLSessionConfiguration provides a few factory methods to create your session configuration. The methods are:

  • +defaultConfiguration – provides access to the global singleton storage and settings
  • +ephemeralSessionConfiguration – a private, in-memory only storage
  • +backgroundSessionConfiguration: – out-of-process configuration that is keyed to the identifier string. This identifier string is something you specify and must be unique for your app. It is used to retrieve the upload or download response by re-attaching to the previous session that used the identifier.

NSURLSessionTask is the class that actually replaces the NSURLConnection class. NSURLSessionTask provides properties that will show status and progress for the task as opposed to only being available in delegate methods. NSURLSessionTask supports the canceling, suspending or resuming an operation. There are a couple of NSURLSessionTask sub-classes that have been provided to differentiate between types of tasks: Data, Upload and Download. The upload and download tasks are the only types of tasks that can be run out-of-process, and the download task is the only task that can be paused and then resumed later.

There are also several delegates available in the NSURLSession API:

  • NSURLSessionDelegate – The single delegate for all NSURLSession messages. This applies to the session, task, data task and download task. TheNSURLSessionDelegate is strongly referenced and will exist until the session has been invliadted
  • NSURLSessionTaskDelegate – A delegate that provides messages for data, download or upload tasks.
  • NSURLSessionDataDelegate – Provides additional messages specific to data tasks.
  • NSURLSessionDownloadDelegate – Provides additional messages specific to download tasks.

NSURLSession is the class that manages all of the tasks. You use the NSURLSessionConfiguration class to configure your NSURLSession. You can use the public global configuration or you can provide custom sessions with private configuration with your sessions. The NSURLSession creates all of your data, upload and download task objects. It also provides several asynchronous convenience methods to perform operations without having to create your own tasks.

Downloading an Image Using NSURLSession

Now that we’ve been introduced to all the objects that make up NSURLSession API, let’s move on to writing code to download an image with NSURLSession. The sample application has two buttons: one to download the image in-process and the other to download the image in the background. To create our in-process session we simply do the following:

NSURLSessionConfiguration *configuration = 
    [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = 
     [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

As you can see we are using the defaultSessionConfiguration to utilize the public storage area with our session. In this case we are performing all of the network interaction code inside of an NSOperation subclass that implements our delegate methods. By setting the delegateQueue to nil, we are letting the system create a serial operation queue for performing all delegate method calls and completion handler calls.

As I mentioned before we also want to be able to support downloading the image in a background process so that if the user starts the image download and switches to another app, the download will complete anyway. However, if the app is manually quit by the user or pruned by the OS and the download has not completed, it will not continue in the background. The code to create the background session looks like this:

NSURLSessionConfiguration *backgroundConfiguration 
   = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.captech.NSURLSample.BackgroundSession"];
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration
                                                          delegate:self
                                                     delegateQueue:nil];

This is very similar to the logic to create our in-process session with the exception of how the session configuration is created. In this case we create a background session named com.captech.NSURLSample.BackgroundSession.

Now that we have our session created, the next step is to create our download task:

NSURL *downloadURL = [NSURL URLWithString:self.downloadUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
self.downloadTask = [self.session downloadTaskWithRequest:request];
        [self.downloadTask resume];

First we create our an NSURLRequest with that URL passed into the operation. Creating the download task is as simple as calling thedownloadTaskWithRequest: method using the NSURLRequest we just made. You’ll notice after the task is created we have to call the resume method because by default all tasks that are created start out in the suspended state, and it won’t actually begin without calling it.

As I mentioned earlier, download tasks provide the ability to report on progress of the download as it’s occurring. The sample app has a progress indicator to show the progress of the image download. To wire this up we just have to implement:

URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:

in the class conforming to NSURLSessionDownloadDelegate. This looks like this:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
   didWriteData:(int64_t)bytesWritten 
    totalBytesWritten:(int64_t)totalBytesWritten 
     totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    if (downloadTask == self.downloadTask && self.progressAction){
        self.progressAction((double)totalBytesWritten, (double)totalBytesExpectedToWrite);
    }
}

Since the delegate is actually my NSOperation class running off the main thread, I didn’t want to directly update the UI in this method. Instead the operation has a block property (not shown below) where the UI can specify what happens when this didWriteData is called. The logic to actually update the progress indicator looks like this:

        double progress = bytesWritten / bytesExpected;
dispatch_async(dispatch_get_main_queue(), ^{
    self.progressView.progress = (float) progress;
});

We simply calculate the progress and update the indicator in the UI. Once the download task has completed, we need to hide our progress indicator and display the image on our screen. The sample app saves the image to the file system when complete:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
  didFinishDownloadingToURL:(NSURL *)location {
    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 *error;

    [fileManager removeItemAtURL:destinationUrl error:NULL];
    BOOL success = [fileManager copyItemAtURL:location toURL:destinationUrl error:&error];
    if (self.completionAction){
        self.completionAction(destinationUrl, success);
    }
}

Just like with the progress updates, we won’t do any UI updates from our operation so there is a block on the operation (not shown in code sample below) to handle the updates. That logic looks like this:

dispatch_async(dispatch_get_main_queue(), ^{
    if (success){
        UIImage *image = [UIImage imageWithContentsOfFile:[imageUrl path]];
        self.downloadedImage.image = image;
    }
    self.downloadedImage.hidden = NO;
    self.progressView.progress = 0;
    self.progressView.hidden = YES;
    self.downloadOperation = nil;
});

If the operation was successful, we retrieve the image from the file system, update the UIImageView, then reset the progress indicator and hide it. Then we set our download operation to nil so that we can download the image again if we want to.

That covers the in-process download and it almost completes the out-of-process download as well, but we have a few other things to take care of to complete this. In our NSURLSessionDelegate we have to implement the URLSessionDidFinishEventsForBackgroundURLSession method so that we can call the app delegate completion handler for the background download. This completion handler lets the system know that your app’s user interface is updated and a new snapshot can be taken. Our implementation looks something like this:

CTAppDelegate *appDelegate = (CTAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
    void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
    appDelegate.backgroundSessionCompletionHandler = nil;
    completionHandler();
}

We store the completion handler as a property in the app delegate and call it once the background download is complete. The completion handler is passed to the app delegate by the operating system whenever the background download has completed. If the app becomes active before the download completes, the delegate will begin receiving callbacks again without any action required by the application.

The logic to actually start the download, either in-process or out-of process looks like this:

if (self.downloadOperation){
    return;
}

CTSessionOperation *operation = [CTSessionOperation new];
operation.downloadUrl = downloadUrl;
operation.progressAction = ^(double bytesWritten, double bytesExpected){
    double progress = bytesWritten / bytesExpected;
    dispatch_async(dispatch_get_main_queue(), ^{
        self.progressView.progress = (float) progress;
    });
};
operation.completionAction = ^(NSURL *imageUrl, BOOL success){
    dispatch_async(dispatch_get_main_queue(), ^{
        if (success){
            UIImage *image = [UIImage imageWithContentsOfFile:[imageUrl path]];
            self.downloadedImage.image = image;
        }
        self.downloadedImage.hidden = NO;
        self.progressView.progress = 0;
        self.progressView.hidden = YES;
        self.downloadOperation = nil;
    });
};
operation.isBackground = background;

[operation enqueueOperation];
self.downloadedImage.hidden = YES;
self.progressView.hidden = NO;
self.downloadOperation = operation;

We create a new instance of our CTSessionOperation and set all of the properties necessary to download our image. We then enqueue the operation on anNSOperationQueue, hide the UIImageView and show our progress indicator. The operation then does all the work to download the image.

As you can see, utilizing NSURLSession API is an easy and powerful way to interact with a network. This is a very simplified example of how you can write code to interact with a network, but the NSURLSession makes it very easy to do.

Thanks 🙂

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s