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.
Session Configuration
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.
Mutability
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.
Default Configuration
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.
Ephemeral Configuration
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
.
Background Configuration
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.
Session Configuration
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.
// 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.
Canceling and Resuming Downloads
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.
Step 1: Outlets and Actions
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.
#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
Step 2: User Interface
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.
Step 3: Refactoring
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.
#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.
- (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
.
- (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]; }
Step 4: Canceling the Download
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.
- (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]; }]; }
Step 5: Resuming the Download
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
.
- (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.
Step 6: Finishing Touches
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.
- (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?
- (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)]; }); } }
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.
Step 7: Invalidating the Session
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.
- (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]; }
Conclusion
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.
Comments