Sunday, 21 December 2014

Networking with NSURLSession: Part 2

his post is part of a series called Working with NSURLSession.
Networking with NSURLSession: Part 1
Working with NSURLSession: Part 3
In the previous tutorial, I introduced you to NSURLSession. I talked about the advantages it has over NSURLConnection and how to use NSURLSession for simple tasks, such as fetching data from a web service and downloading an image from the web. In this tutorial, we'll take a closer look at the configuration options of NSURLSession and how to cancel and resume a download task. We've got a lot of ground to cover so let's get started.

As we saw in the previous tutorial, a session, an instance of NSURLSession, is a configurable container for putting network requests into. The configuration of the session is handled by an instance of NSURLSessionConfiguration.
A session configuration object is nothing more than a dictionary of properties that defines how the session it is tied to behaves. A session has one session configuration object that dictates cookie, security, and cache policies, the maximum number of connections to a host, resource and network timeouts, etc. This is a significant improvement over NSURLConnection, which relied on a global configuration object with much less flexibility.
Once a session is created and configured by a NSURLSessionConfiguration instance, the session's configuration cannot be modified. If you need to modify a session's configuration, you have to create a new session. Keep in mind that it is possible to copy a session's configuration and modify it, but the changes have no effect on the session from which the configuration was copied.
The NSURLSessionConfiguration class provides three factory constructors for instantiating standard configurations, defaultSessionConfiguration, ephemeralSessionConfiguration, and backgroundSessionConfiguration. The first method returns a copy of the default session configuration object, which results in a session that behaves similarly to an NSURLConnection object in its standard configuration. Altering a session configuration obtained through the defaultSessionConfiguration factory method doesn't change the default session configuration which it's a copy of.
A session configuration object created by invoking the ephemeralSessionConfiguration factory method ensures that the resulting session uses no persistent storage for cookies, caches, or credentials. In other words, cookies, caches, and credentials are kept in memory. Ephemeral sessions are therefore ideal if you need to implement private browsing, something that simply wasn't possible before the introduction of NSURLSession.
The backgroundSessionConfiguration: factory method creates a session configuration object that enables out-of-process uploads and downloads. The upload and download tasks are managed by a background daemon and continue to run even if the application is suspended or crashes. We'll talk more about background sessions later in this series.
As we saw in the previous tutorial, creating a session configuration object is simple. In the example shown below, I used the defaultSessionConfiguration factory method to create a NSURLSessionConfiguration instance. Configuring a session configuration object is as simple as modifying its properties as shown in the example. We can then use the session configuration object to instantiate a session object. The session object serves as a factory for data, upload, and download tasks, with each task corresponding to a single request. In the example below, we query the iTunes Search API as we did in the previous tutorial.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Create Session Configuration
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
 
// Configure Session Configuration
[sessionConfiguration setAllowsCellularAccess:YES];
[sessionConfiguration setHTTPAdditionalHeaders:@{ @"Accept" : @"application/json" }];
 
// Create Session
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
 
// Send Request
NSURL *url = [NSURL URLWithString:@"https://itunes.apple.com/search?term=apple&media=software"];
[[session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]);
}] resume];
The example also illustrates how easy it is to add custom headers by setting the HTTPAdditionalHeaders property of the session configuration object. The beauty of the NSURLSession API is that every request that passes through the session is configured by the session's configuration object. Adding authentication headers to a set of requests, for example, becomes easy as pie.
In the previous tutorial, I showed you how to download an image using the NSURLSession API. However, network connections are unreliable and it happens all too often that a download fails due to a flaky network connection. Fortunately, resuming a download isn't difficult with the NSURLSession API. In the next example, I'll show you how to cancel and resume the download of an image.
Before we take a closer look at resuming a download task, it is important to understand the difference between canceling and suspending a download task. It is possible to suspend a download task and resume it at a later time. Canceling a download task, however, stops the task and it isn't possible to resume it at a later time. There is one alternative, though. It is possible to cancel a download task by calling cancelByProducingResumeData: on it. It accepts a completion handler that accepts one parameter, an NSData object that is used to resume the download at a later time by invoking downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler: on a session object. The NSData object contains the necessary information to resume the download task where it left off.
Open the project we created in the previous tutorial or download it here. We start by adding two buttons to the user interface, one to cancel the download and one to resume the download. In the view controller's header file, create an outlet and an action for each button as shown below.
01
02
03
04
05
06
07
08
09
10
11
12
13
#import <UIKit/UIKit.h>
 
@interface MTViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UIButton *cancelButton;
@property (weak, nonatomic) IBOutlet UIButton *resumeButton;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
 
- (IBAction)cancel:(id)sender;
- (IBAction)resume:(id)sender;
 
@end
Open the project's main storyboard and add two buttons to the view controller's view. Position the buttons as shown in the screenshot below and connect each button with its corresponding outlet and action.
Update the user interface.
We'll need to do some refactoring to make everything work correctly. Open MTViewController.m and declare one instance variable and two properties. The instance variable, session, will keep a reference to the session we'll use for downloading the image.
01
02
03
04
05
06
07
08
09
10
#import "MTViewController.h"
 
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate> {
    NSURLSession *_session;
}
 
@property (strong, nonatomic) NSURLSessionDownloadTask *downloadTask;
@property (strong, nonatomic) NSData *resumeData;
 
@end
We also need to refactor the viewDidLoad method, but first I'd like to implement a getter method for the session. Its implementation is pretty straightforward as you can see below. We create a session configuration object using the defaultSessionConfiguration factory method and instantiate the session object with it. The view controller serves as the session's delegate.
01
02
03
04
05
06
07
08
09
10
11
- (NSURLSession *)session {
    if (!_session) {
        // Create Session Configuration
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
 
        // Create Session
        _session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
    }
 
    return _session;
}
With the session accessor implemented, the viewDidLoad method becomes much simpler. We create a download task, as we did in the previous tutorial, and store a reference to the task in downloadTask. We then tell the download task to resume.
1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
    [super viewDidLoad];
 
    // Create Download Task
    self.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:@"http://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg"]];
 
    // Resume Download Task
    [self.downloadTask resume];
}
The cancel: action contains the logic for canceling the download task we just created. If downloadTask is not nil, we call cancelByProducingResumeData: on the task. This method accepts one parameter, a completion block. The completion block also takes one parameter, an instance of NSData. If resumeData is not nil, we store a reference to the data object in view controller's resumeData property.
If a download is not resumable, the completion block's resumeData parameter is nil. Not every download is resumable so it's important to check if resumeData is a valid NSData object.
01
02
03
04
05
06
07
08
09
10
11
12
- (IBAction)cancel:(id)sender {
    if (!self.downloadTask) return;
 
    // Hide Cancel Button
    [self.cancelButton setHidden:YES];
 
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        if (!resumeData) return;
        [self setResumeData:resumeData];
        [self setDownloadTask:nil];
    }];
}
Resuming the download task after it was canceled is easy. In the resume: action, we check if the view controller's resumeData property is set. If resumeData is a valid NSData object, we tell the session object to create a new download task and pass it the NSData object. This is all the session object needs to recreate the download task that we canceled in the cancel: action. We then tell the download task to resume and set resumeData to nil.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
- (IBAction)resume:(id)sender {
    if (!self.resumeData) return;
 
    // Hide Resume Button
    [self.resumeButton setHidden:YES];
 
    // Create Download Task
    self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
 
    // Resume Download Task
    [self.downloadTask resume];
 
    // Cleanup
    [self setResumeData:nil];
}
Build the project and run the application in the iOS Simulator or on a physical device. The download should start automatically. Tap the cancel button to cancel the download and tap the resume button to resume the download.
There are a number of details we need to take care of. First of all, the buttons shouldn't always be visible. We'll use key value observing to show and hide the buttons when necessary. In viewDidLoad, hide the buttons and add the view controller as an observer of itself for the resumeData and downloadTask key paths.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
    [super viewDidLoad];
 
    // Add Observer
    [self addObserver:self forKeyPath:@"resumeData" options:NSKeyValueObservingOptionNew context:NULL];
    [self addObserver:self forKeyPath:@"downloadTask" options:NSKeyValueObservingOptionNew context:NULL];
 
    // Setup User Interface
    [self.cancelButton setHidden:YES];
    [self.resumeButton setHidden:YES];
 
    // Create Download Task
    self.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:@"http://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg"]];
 
    // Resume Download Task
    [self.downloadTask resume];
}
In observeValueForKeyPath:ofObject:change:context:, we hide the cancel button if resumeData is nil and we hide the resume button if downloadTask is nil. Build the project and run the application one more time to see the result. This is better. Right?
01
02
03
04
05
06
07
08
09
10
11
12
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"resumeData"]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.resumeButton setHidden:(self.resumeData == nil)];
        });
         
    } else if ([keyPath isEqualToString:@"downloadTask"]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.cancelButton setHidden:(self.downloadTask == nil)];
        });
    }
}
As George Yang points out in the comments, we don't know whether observeValueForKeyPath:ofObject:change:context: is called on the main thread. It is therefore important to update the user interface in a GCD (Grand Central Dispatch) block that is invoked on the main queue.
Advertisement
There is one key aspect of NSURLSession that I haven't talked about yet, session invalidation. The session keeps a strong reference to its delegate, which means that the delegate isn't released as long as the session is active. To break this reference cycle, the session needs to be invalidated. When a session is invalidated, active tasks are canceled or finished, and the delegate is sent a URLSession:didBecomeInvalidWithError: message and the session releases its delegate.
There are several places that we can invalidate the session. Since the view controller downloads only one image, the session can be invalidated when the download finishes. Take a look at the updated implementation of URLSession:downloadTask:didFinishDownloadingToURL:. The cancel button is also hidden when the download finishes.
01
02
03
04
05
06
07
08
09
10
11
12
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSData *data = [NSData dataWithContentsOfURL:location];
 
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.cancelButton setHidden:YES];
        [self.progressView setHidden:YES];
        [self.imageView setImage:[UIImage imageWithData:data]];
    });
 
    // Invalidate Session
    [session finishTasksAndInvalidate];
}
The example project we created in this tutorial is a simplified implementation of how to cancel and resume downloads. In your applications, it may be necessary to write the resumeData object to disk for later use and it may be possible that several download tasks are running at the same time. Even though this adds complexity, the basic principles remain the same. Be sure to prevent memory leaks by always invalidating a session that you no longer need.

No comments:

Post a Comment