Working with NSURLSession: Part 4
In the previous tutorial, we started creating a simple podcast client to put what we've learned about
When an upload or download is initiated, a background daemon comes into existence. The daemon takes care of the task and sends updates to the application through the delegate protocols declared in the
1. Subclass
We override the class's designated initializer as shown below. Note how we ignore the
In the initializer, we set the background color of the text and detail text labels to
Before we take a look at the implementation of
In
Step 5: Use
To make use of the
In the view controller's
In
Before we build the project and run the application, we need to update our implementation of
Step 1: Create
Start by declaring a new property
In
Let's take a look at the implementation of
In the
In
As you may have guessed,
We're not done yet. Is the compiler giving you three warnings? That's not surprising as we haven't implemented the required methods of the
More interesting is the implementation of
The implementation of
The third delegate method of the
In
In
In
The session delegate is notified when a download task finishes. In
In our example, we don't need to do anything special to make sure that our application reconnects to the original background session. This is taken care of by the
When our application is woken up by the operating system to respond to the notifications of the background session, the application delegate is sent a message of
We then implement another helper method,
Wait a minute. When do we invoke
NSURLSession
into practice. So far, our podcast client can query the iTunes Search API, download a podcast feed, and display a list of episodes. In this tutorial, we zoom in on another interesting aspect of NSURLSession
, out-of-process downloads. Let me show you how this works.Introduction
In this fourth and final tutorial aboutNSURLSession
, we'll take a closer look at out-of-process tasks, download tasks in particular. Our podcast client is already able to show a list of episodes, but it currently lacks the ability to download individual episodes. That'll be the focus of this tutorial.Background Uploads and Downloads
Adding support for background uploads and downloads is surprisingly easy withNSURLSession
. Apple refers to them as out-of-process uploads and downloads as the tasks are managed by a background daemon, not your application. Even if your application crashes during an upload or download task, the task continues in the background.Overview
I'd like to take a few moments to take a closer look at how out-of-process tasks work. It's pretty simple once you have a complete picture of the process. Enabling background uploads and downloads is nothing more than flipping a switch in your session's configuration. With a properly configured session object, you are ready to schedule upload and download tasks in the background.When an upload or download is initiated, a background daemon comes into existence. The daemon takes care of the task and sends updates to the application through the delegate protocols declared in the
NSURLSession
API. If your application stops running for some reason, the task continues in the background as it's the daemon managing the task. The moment the task finishes, the application that created the task is notified. It reconnects with the background session that created the task and the daemon managing the task informs the session that the task finished and, in the case of a download task, hands the file over to the session. The session then invokes the appropriate delegate methods to make sure your application can take the appropriate actions, such as moving the file to a more permanent location. That's enough theory for now. Let's see what we need to do to implement out-of-process downloads in Singlecast.1. Subclass UITableViewCell
Step 1: Update Main Storyboard
At the moment, we are using prototype cells to populate the table view. To give us a bit more flexibility, we need to create aUITableViewCell
subclass. Open the main storyboard, select the table view of the MTViewController
instance and set the number of prototype cells to 0
.Step 2: Create Subclass
Open Xcode's File menu and choose New > File.... Create a new Objective-C class, name itMTEpisodeCell
, and make sure it inherits from UITableViewCell
. Tell Xcode where you'd like to store the class files and hit Create.Step 3: Update Class Interface
The interface ofMTEpisodeCell
is simple as you can see in the code snippet below. All we do is declare a property progress
of type float
. We'll use this to update and display the progress of the download task that we'll use for downloading an episode.1 2 3 4 5 6 7 | #import <UIKit/UIKit.h> @interface MTEpisodeCell : UITableViewCell @property (assign, nonatomic ) float progress; @end |
Step 4: Implement Class
The implementation ofMTEpisodeCell
is a bit more involved, but it isn't complicated. Instead of using an instance of UIProgressView
, we'll fill the cell's content view with a solid color to show the progress of the download task. We do this by adding a subview to the cell's content view and updating its width whenever the cell's progress
property changes. Start by declaring a private property progressView
of type UIView
.1 2 3 4 5 6 7 | #import "MTEpisodeCell.h" @interface MTEpisodeCell () @property (strong, nonatomic ) UIView *progressView; @end |
style
argument and pass UITableViewCellStyleSubtitle
to the superclass's designated initializer. This is important, because the table view will pass UITableViewCellStyleDefault
as the cell's style when we ask it for a new cell.In the initializer, we set the background color of the text and detail text labels to
[UIColor clearColor]
and create the progress view. Two details are especially important. First, we insert the progress view as a subview of the cell's content view at index 0
to make sure that it's inserted below the text labels. Second, we invoke updateView
to make sure that the frame of the progress view is updated to reflect the value of progress
, which is set to 0
during the cell's initialization.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | - ( id )initWithStyle:(UITableViewCellStyle)style reuseIdentifier :( NSString *)reuseIdentifier { self = [ super initWithStyle : UITableViewCellStyleSubtitle reuseIdentifier :reuseIdentifier]; if ( self ) { // Helpers CGSize size = self .contentView .bounds .size ; // Configure Labels [ self .textLabel setBackgroundColor :[ UIColor clearColor ]]; [ self .detailTextLabel setBackgroundColor :[ UIColor clearColor ]]; // Initialize Progress View self .progressView = [[ UIView alloc ] initWithFrame :CGRectMake( 0 .0 , 0 .0 , size .width , size .height )]; // Configure Progress View [ self .progressView setAutoresizingMask :( UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleWidth)]; [ self .progressView setBackgroundColor :[ UIColor colorWithRed : 0 .678 green : 0 .886 blue : 0 .557 alpha : 1 .0 ]]; [ self .contentView insertSubview : self .progressView atIndex : 0 ]; // Update View [ self updateView ]; } return self ; } |
updateView
, we need to override the setter method of the progress
property. The only change we make to the default implementation of setProgress:
is invoke updateView
when the _progress
instance variable is updated. This ensures that the progress view is updated whenever we update the cell's progress
property.1 2 3 4 5 6 7 8 | - ( void )setProgress:(CGFloat)progress { if (_progress != progress) { _progress = progress; // Update View [ self updateView ]; } } |
updateView
, we calculate the new width of the progress view based on the value of the cell's progress
property.1 2 3 4 5 6 7 8 9 | - ( void )updateView { // Helpers CGSize size = self .contentView .bounds .size ; // Update Frame Progress View CGRect frame = self .progressView .frame ; frame .size .width = size .width * self .progress ; self .progressView .frame = frame; } |
Step 5: Use MTEpisodeCell
To make use of the MTEpisodeCell
, we need to make a few changes in the MTViewController
class. Start by adding an import statement for MTEpisodeCell
.01 02 03 04 05 06 07 08 09 10 11 12 13 | #import "MTViewController.h" #import "MWFeedParser.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () <MWFeedParserDelegate> @property (strong, nonatomic ) NSDictionary *podcast; @property (strong, nonatomic ) NSMutableArray *episodes; @property (strong, nonatomic ) MWFeedParser *feedParser; @end |
viewDidLoad
method, invoke setupView
, a helper method we'll implement next.01 02 03 04 05 06 07 08 09 10 11 12 | - ( void )viewDidLoad { [ super viewDidLoad ]; // Setup View [ self setupView ]; // Load Podcast [ self loadPodcast ]; // Add Observer [[ NSUserDefaults standardUserDefaults ] addObserver : self forKeyPath : @"MTPodcast" options : NSKeyValueObservingOptionNew context : NULL ]; } |
setupView
, we invoke setupTableView
, another helper method in which we tell the table view to use the MTEpisodeCell
class whenever it needs a cell with a reuse identifier of EpisodeCell
.1 2 3 4 | - ( void )setupView { // Setup Table View [ self setupTableView ]; } |
1 2 3 4 | - ( void )setupTableView { // Register Class for Cell Reuse [ self .tableView registerClass :[ MTEpisodeCell class ] forCellReuseIdentifier :EpisodeCell]; } |
tableView:cellForRowAtIndexPath:
as shown below.01 02 03 04 05 06 07 08 09 10 11 12 | - ( UITableViewCell *)tableView:( UITableView *)tableView cellForRowAtIndexPath :( NSIndexPath *)indexPath { MTEpisodeCell *cell = ( MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier : EpisodeCell forIndexPath :indexPath]; // Fetch Feed Item MWFeedItem *feedItem = [ self .episodes objectAtIndex :indexPath .row ]; // Configure Table View Cell [cell .textLabel setText :feedItem .title ]; [cell .detailTextLabel setText :[ NSString stringWithFormat : @"%@" , feedItem .date ]]; return cell; } |
Step 6: Build and Run
Run your application in the iOS Simulator or on a test device to see the result. If nothing has changed, then you've followed the steps correctly. All that we've done so far is replacing the prototype cells with instances ofMTEpisodeCell
.2. Create Background Session
To enable out-of-process downloads, we need a session that is configured to support out-of-process downloads. This is surprisingly easy to do with theNSURLSession
API. There a few gotchas though.Step 1: Create session
Property
Start by declaring a new property session
of type NSURLSession
in the MTViewController
class and make the class conform to the NSURLSessionDelegate
and NSURLSessionDownloadDelegate
protocols.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | #import "MTViewController.h" #import "MWFeedParser.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () < NSURLSessionDelegate , NSURLSessionDownloadDelegate , MWFeedParserDelegate> @property (strong, nonatomic ) NSDictionary *podcast; @property (strong, nonatomic ) NSMutableArray *episodes; @property (strong, nonatomic ) MWFeedParser *feedParser; @property (strong, nonatomic ) NSURLSession *session; @end |
viewDidLoad
, we set the session
property by invoking backgroundSession
on the view controller instance. This is one of the gotchas I was talking about.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | - ( void )viewDidLoad { [ super viewDidLoad ]; // Setup View [ self setupView ]; // Initialize Session [ self setSession :[ self backgroundSession ]]; // Load Podcast [ self loadPodcast ]; // Add Observer [[ NSUserDefaults standardUserDefaults ] addObserver : self forKeyPath : @"MTPodcast" options : NSKeyValueObservingOptionNew context : NULL ]; } |
backgroundSession
. In backgroundSession
, we statically declare a session
variable and use dispatch_once
(Grand Central Dispatch) to instantiate the background session. Even though this isn't strictly necessary, it emphasizes the fact that we only need one background session at any time. This is a best practice that's also mentioned in the WWDC session on the NSURLSession
API.In the
dispatch_once
block, we start by creating a NSURLSessionConfiguration
object by invoking backgroundSessionConfiguration:
and passing a string as an identifier. The identifier we pass uniquely identifies the background session, which is key as we'll see a bit later. We then create a session instance by invoking sessionWithConfiguration:delegate:delegateQueue:
and passing the session configuration object, setting the session's delegate
property, and passing nil
as the third argument.01 02 03 04 05 06 07 08 09 10 11 12 13 | - ( NSURLSession *)backgroundSession { static NSURLSession *session = nil ; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Session Configuration NSURLSessionConfiguration *sessionConfiguration = [ NSURLSessionConfiguration backgroundSessionConfiguration : @"com.mobiletuts.Singlecast.BackgroundSession" ]; // Initialize Session session = [ NSURLSession sessionWithConfiguration :sessionConfiguration delegate : self delegateQueue :nil ]; }); return session; } |
By passing
nil
as the third argument of sessionWithConfiguration:delegate:delegateQueue:
, the session creates a serial operation queue for us. This operation queue is used for performing the delegate method calls and completion handler calls. 3. Download Episode
Step 1: Create Download Task
It's time to make use of the background session we created and put theMTEpisodeCell
to use. Let's start by implementing tableView:didSelectRowAtIndexPath:
, a method of the UITableViewDelegate
protocol. Its implementation is straightforward as you can see below. We fetch the correct MWFeedItem
instance from the episodes
array and pass it to downloadEpisodeWithFeedItem:
.1 2 3 4 5 6 7 8 9 | - ( void )tableView:( UITableView *)tableView didSelectRowAtIndexPath :( NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath :indexPath animated : YES ]; // Fetch Feed Item MWFeedItem *feedItem = [ self .episodes objectAtIndex :indexPath .row ]; // Download Episode with Feed Item [ self downloadEpisodeWithFeedItem :feedItem]; } |
downloadEpisodeWithFeedItem:
, we extract the remote URL from the feed item by invoking urlForFeedItem:
, create a download task by calling downloadTaskWithURL:
on the background session, and send it a message of resume
to start the download task.1 2 3 4 5 6 7 8 9 | - ( void )downloadEpisodeWithFeedItem:( MWFeedItem *)feedItem { // Extract URL for Feed Item NSURL * URL = [ self urlForFeedItem :feedItem]; if (URL) { // Schedule Download Task [[ self .session downloadTaskWithURL :URL] resume ]; } } |
urlForFeedItem:
is a convenience method that we use. We'll use it a few more times in this project. We obtain a reference to the feed item's enclosures
array, extract the first enclosure, and pull out the object for the url
key. We create and return an NSURL
instance.01 02 03 04 05 06 07 08 09 10 11 12 13 | - ( NSURL *)urlForFeedItem:( MWFeedItem *)feedItem { NSURL *result = nil ; // Extract Enclosures NSArray *enclosures = [feedItem enclosures ]; if (!enclosures || !enclosures .count ) return result; NSDictionary *enclosure = [enclosures objectAtIndex : 0 ]; NSString *urlString = [enclosure objectForKey : @"url" ]; result = [ NSURL URLWithString :urlString]; return result; } |
NSURLSessionDelegate
and NSURLSessionDownloadDelegate
protocols yet. We also need to implement these methods if we want to show the progress of the download tasks.Step 2: Implementing Protocol(s)
The first method we need to implement isURLSession:downloadTask:didResumeAtOffset:
. This method is invoked if a download task is resumed. Because this is something we won't cover in this tutorial, we simply log a message to Xcode's console.1 2 3 | - ( void )URLSession:( NSURLSession *)session downloadTask :( NSURLSessionDownloadTask *)downloadTask didResumeAtOffset :(int 6 4 _t)fileOffset expectedTotalBytes :(int 6 4 _t)expectedTotalBytes { NSLog( @"%s" , __PRETTY_FUNCTION__); } |
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
. This method is invoked every time a few bytes have been downloaded by the session. In this delegate method, we calculate the progress, fetch the correct cell, and update the cell's progress property, which in turn updates the cell's progress view. Have you spotted the dispatch_async
call? There's no guarantee that the delegate method is invoked on the main thread. Since we update the user interface by setting the cell's progress, we need to update the cell's progress
property on the main thread.01 02 03 04 05 06 07 08 09 10 11 | - ( void )URLSession:( NSURLSession *)session downloadTask :( NSURLSessionDownloadTask *)downloadTask didWriteData :(int 6 4 _t)bytesWritten totalBytesWritten :(int 6 4 _t)totalBytesWritten totalBytesExpectedToWrite :(int 6 4 _t)totalBytesExpectedToWrite { // Calculate Progress double progress = ( double )totalBytesWritten / ( double )totalBytesExpectedToWrite; // Update Table View Cell MTEpisodeCell *cell = [ self cellForForDownloadTask :downloadTask]; dispatch_async(dispatch_get_main_queue(), ^{ [cell setProgress :progress]; }); } |
cellForForDownloadTask:
is straightforward. We pull the remote URL from the download task using its originalRequest
property and loop over the feed items in the episodes
array until we have a match. When we've found a match, we ask the table view for the corresponding cell and return it.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | - ( MTEpisodeCell *)cellForForDownloadTask:( NSURLSessionDownloadTask *)downloadTask { // Helpers MTEpisodeCell *cell = nil ; NSURL * URL = [[downloadTask originalRequest ] URL ]; for ( MWFeedItem *feedItem in self .episodes ) { NSURL *feedItemURL = [ self urlForFeedItem :feedItem]; if ([ URL isEqual :feedItemURL]) { NSUInteger index = [ self .episodes indexOfObject :feedItem]; cell = ( MTEpisodeCell *)[ self .tableView cellForRowAtIndexPath :[ NSIndexPath indexPathForRow :index inSection : 0 ]]; break; } } return cell; } |
NSURLSessionDownloadDelegate
protocol that we need to implement is URLSession:downloadTask:didFinishDownloadingToURL:
. As I mentioned in the previous tutorials, one of the advantages of the NSURLSession
API is that downloads are immediately written to disk. The result is that we are passed a local URL in URLSession:downloadTask:didFinishDownloadingToURL:
. However, the local URL that we receive, points to a temporary file. It is our responsibility to move the file to a more permanent location and that's exactly what we do in URLSession:downloadTask:didFinishDownloadingToURL:
.1 2 3 4 | - ( void )URLSession:( NSURLSession *)session downloadTask :( NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL :( NSURL *)location { // Write File to Disk [ self moveFileWithURL :location downloadTask :downloadTask]; } |
moveFileWithURL:downloadTask:
, we extract the episode's file name from the download task and create a URL in the application's Documents directory by invoking URLForEpisodeWithName:
. If the temporary file that we received from the background session points to a valid file, we move that file to its new home in the application's Documents directory.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | - ( void )moveFileWithURL:( NSURL *) URL downloadTask :( NSURLSessionDownloadTask *)downloadTask { // Filename NSString *fileName = [[[downloadTask originalRequest ] URL ] lastPathComponent ]; // Local URL NSURL *localURL = [ self URLForEpisodeWithName :fileName]; NSFileManager *fm = [ NSFileManager defaultManager ]; if ([fm fileExistsAtPath :[ URL path ]]) { NSError *error = nil ; [fm moveItemAtURL : URL toURL :localURL error :&error]; if (error) { NSLog( @"Unable to move temporary file to destination. %@, %@" , error, error .userInfo ); } } } |
I use a lot of helper methods in my iOS projects, because it makes for DRY code. It's also good practice to create methods that only do one thing. Testing becomes much easier that way.
URLForEpisodeWithName:
is another helper method, which invokes episodesDirectory
. In URLForEpisodeWithName:
, we append the name
argument to the Episodes directory, which is located in the application's Documents directory.1 2 3 4 | - ( NSURL *)URLForEpisodeWithName:( NSString *)name { if (!name) return nil ; return [ self .episodesDirectory URLByAppendingPathComponent :name]; } |
episodesDirectory
, we create the URL for the Episodes directory and create the directory if it doesn't exist yet.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | - ( NSURL *)episodesDirectory { NSURL *documents = [[[ NSFileManager defaultManager ] URLsForDirectory : NSDocumentDirectory inDomains :NSUserDomainMask] lastObject ]; NSURL *episodes = [documents URLByAppendingPathComponent : @"Episodes" ]; NSFileManager *fm = [ NSFileManager defaultManager ]; if (![fm fileExistsAtPath :[episodes path ]]) { NSError *error = nil ; [fm createDirectoryAtURL :episodes withIntermediateDirectories : YES attributes :nil error :&error]; if (error) { NSLog( @"Unable to create episodes directory. %@, %@" , error, error .userInfo ); } } return episodes; } |
Step 3: Build and Run
Run the application and test the result by downloading an episode from the list of episodes. You should see the table view cell's progress view progress from left to right reflecting the progress of the download task. There are a few issues though. Have you tried scrolling through the table view? That doesn't look right. Let's fix that.4. Create a Progress Buffer
Because the table view reuses cells as much as possible, we need to make sure that each cell properly reflects the download state of the episode that it represents. We can fix this in several ways. One approach is to use an object that keeps track of the progress of each download task, including the download tasks that have already completed.Step 1: Declare a Property
Let's start by declaring a new private propertyprogressBuffer
of type NSMutableDictionary
in the MTViewController
class.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | #import "MTViewController.h" #import "MWFeedParser.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () < NSURLSessionDelegate , NSURLSessionDownloadDelegate , MWFeedParserDelegate> @property (strong, nonatomic ) NSDictionary *podcast; @property (strong, nonatomic ) NSMutableArray *episodes; @property (strong, nonatomic ) MWFeedParser *feedParser; @property (strong, nonatomic ) NSURLSession *session; @property (strong, nonatomic ) NSMutableDictionary *progressBuffer; @end |
Step 2: Initialize Buffer
InviewDidLoad
, we initialize the progress buffer as shown below.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | - ( void )viewDidLoad { [ super viewDidLoad ]; // Setup View [ self setupView ]; // Initialize Session [ self setSession :[ self backgroundSession ]]; // Initialize Progress Buffer [ self setProgressBuffer :[ NSMutableDictionary dictionary ]]; // Load Podcast [ self loadPodcast ]; // Add Observer [[ NSUserDefaults standardUserDefaults ] addObserver : self forKeyPath : @"MTPodcast" options : NSKeyValueObservingOptionNew context : NULL ]; } |
Step 3: Update Table View Cells
The key that we'll use in the dictionary is the remote URL of the corresponding feed item. With this in mind, we can update thetableView:cellForRowAtIndexPath:
method as shown below. We pull the remote URL from the feed item and ask progressBuffer
for the value for the key that corresponds to the remote URL. If the value isn't nil
, we set the cell's progress
property to that value, otherwise we set the progress
property of the cell to 0.0
, which hides the progress view by setting its width to 0.0
.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | - ( UITableViewCell *)tableView:( UITableView *)tableView cellForRowAtIndexPath :( NSIndexPath *)indexPath { MTEpisodeCell *cell = ( MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier : EpisodeCell forIndexPath :indexPath]; // Fetch Feed Item MWFeedItem *feedItem = [ self .episodes objectAtIndex :indexPath .row ]; NSURL * URL = [ self urlForFeedItem :feedItem]; // Configure Table View Cell [cell .textLabel setText :feedItem .title ]; [cell .detailTextLabel setText :[ NSString stringWithFormat : @"%@" , feedItem .date ]]; NSNumber *progress = [ self .progressBuffer objectForKey :[ URL absoluteString ]]; if (!progress) progress = @( 0 .0 ); [cell setProgress :[progress floatValue ]]; return cell; } |
Step 4: Avoid Duplicates
We can also use the progress buffer to prevent users from downloading the same episode twice. Take a look at the updated implementation oftableView:didSelectRowAtIndexPath:
. We take the same steps we took in tableView:cellForRowAtIndexPath:
to extract the progress value from the progress buffer. Only when the progress value is nil
, we download the episode.01 02 03 04 05 06 07 08 09 10 11 12 13 14 | - ( void )tableView:( UITableView *)tableView didSelectRowAtIndexPath :( NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath :indexPath animated : YES ]; // Fetch Feed Item MWFeedItem *feedItem = [ self .episodes objectAtIndex :indexPath .row ]; // URL for Feed Item NSURL * URL = [ self urlForFeedItem :feedItem]; if (![ self .progressBuffer objectForKey :[ URL absoluteString ]]) { // Download Episode with Feed Item [ self downloadEpisodeWithFeedItem :feedItem]; } } |
Step 5: Update Buffer
The progress buffer only works in its current implementation if we keep it up to date. This means that we need to update theURLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
method as well. All we do is store the new progress value in the progress buffer.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | - ( void )URLSession:( NSURLSession *)session downloadTask :( NSURLSessionDownloadTask *)downloadTask didWriteData :(int 6 4 _t)bytesWritten totalBytesWritten :(int 6 4 _t)totalBytesWritten totalBytesExpectedToWrite :(int 6 4 _t)totalBytesExpectedToWrite { // Calculate Progress double progress = ( double )totalBytesWritten / ( double )totalBytesExpectedToWrite; // Update Progress Buffer NSURL * URL = [[downloadTask originalRequest ] URL ]; [ self .progressBuffer setObject :@(progress) forKey :[ URL absoluteString ]]; // Update Table View Cell MTEpisodeCell *cell = [ self cellForForDownloadTask :downloadTask]; dispatch_async(dispatch_get_main_queue(), ^{ [cell setProgress :progress]; }); } |
downloadEpisodeWithFeedItem:
, we set the progress value to 0.0
when the download task starts.01 02 03 04 05 06 07 08 09 10 11 12 | - ( void )downloadEpisodeWithFeedItem:( MWFeedItem *)feedItem { // Extract URL for Feed Item NSURL * URL = [ self urlForFeedItem :feedItem]; if (URL) { // Schedule Download Task [[ self .session downloadTaskWithURL :URL] resume ]; // Update Progress Buffer [ self .progressBuffer setObject :@( 0 .0 ) forKey :[ URL absoluteString ]]; } } |
URLSession:downloadTask:didFinishDownloadingToURL:
, we set the progress value to 1.0
.1 2 3 4 5 6 7 8 | - ( void )URLSession:( NSURLSession *)session downloadTask :( NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL :( NSURL *)location { // Write File to Disk [ self moveFileWithURL :location downloadTask :downloadTask]; // Update Progress Buffer NSURL * URL = [[downloadTask originalRequest ] URL ]; [ self .progressBuffer setObject :@( 1 .0 ) forKey :[ URL absoluteString ]]; } |
Step 6: Restore Buffer
At the moment, the progress buffer is only stored in memory, which means that it's cleared between application launches. We could write its contents to disk, but to keep this application simple we are going to restore or recreate the buffer by checking which episodes have already been downloaded. ThefeedParser:didParseFeedItem:
method, part of the MWFeedParserDelegate
protocol, is invoked for every item in the feed. In this method, we pull the remote URL from the feed item, create the corresponding local URL, and check if the file exists. If it does, then we set the corresponding progress value for that feed item to 1.0
to indicate that it's already been downloaded.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | - ( void )feedParser:( MWFeedParser *)parser didParseFeedItem :( MWFeedItem *)item { if (! self .episodes ) { self .episodes = [ NSMutableArray array ]; } [ self .episodes addObject :item]; // Update Progress Buffer NSURL * URL = [ self urlForFeedItem :item]; NSURL *localURL = [ self URLForEpisodeWithName :[ URL lastPathComponent ]]; if ([[ NSFileManager defaultManager ] fileExistsAtPath :[localURL path ]]) { [ self .progressBuffer setObject :@( 1 .0 ) forKey :[ URL absoluteString ]]; } } |
Step 7: Rinse and Repeat
Run the application one more time to see if the issues with the table view are resolved. The application should now also remember which episodes have already been downloaded.5. Being a Good Citizen
It's important that our application is a good citizen by not wasting more CPU cycles or consume more battery power than needed. What does this mean for our podcast client. When a download task is started by our application and the application goes to the background, the background daemon that manages our application's download task notifies our application through the background session that the download task has finished. If necessary, the background daemon will launch our application so that it can respond to these notifications and process the downloaded file.In our example, we don't need to do anything special to make sure that our application reconnects to the original background session. This is taken care of by the
MTViewController
instance. However, we do have to notify the operating system when our application has finished processing the download(s) by invoking a background completion handler.When our application is woken up by the operating system to respond to the notifications of the background session, the application delegate is sent a message of
application:handleEventsForBackgroundURLSession:completionHandler:
. In this method, we can reconnect to the background session, if necessary, and invoke the completion handler that is passed to us. By invoking the completion handler, the operating system knows that our application no longer needs to run in the background. This is important for optimizing battery life. How do we do this in practice?Step 1: Declare a Property
We first need to declare a property on theMTAppDelegate
class to keep a reference to the completion handler that we get from application:handleEventsForBackgroundURLSession:completionHandler:
. The property needs to be public. The reason for this will become clear in a moment.1 2 3 4 5 6 7 8 | #import <UIKit/UIKit.h> @interface MTAppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic ) UIWindow *window; @property ( copy , nonatomic ) void (^backgroundSessionCompletionHandler)(); @end |
Step 2: Implement Callback
Inapplication:handleEventsForBackgroundURLSession:completionHandler:
, we store the completion handler in backgroundSessionCompletionHandler
, which we declared a moment ago.1 2 3 | - ( void )application:( UIApplication *)application handleEventsForBackgroundURLSession :( NSString *)identifier completionHandler :( void (^)())completionHandler { [ self setBackgroundSessionCompletionHandler :completionHandler]; } |
Step 3: Invoke Background Completion Handler
In theMTViewController
class, we start by adding an import statement for the MTAppDelegate
class.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | #import "MTViewController.h" #import "MWFeedParser.h" #import "MTAppDelegate.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () < NSURLSessionDelegate , NSURLSessionDownloadDelegate , MWFeedParserDelegate> @property (strong, nonatomic ) NSDictionary *podcast; @property (strong, nonatomic ) NSMutableArray *episodes; @property (strong, nonatomic ) MWFeedParser *feedParser; @property (strong, nonatomic ) NSURLSession *session; @property (strong, nonatomic ) NSMutableDictionary *progressBuffer; @end |
invokeBackgroundSessionCompletionHandler
, which invokes the background completion handler stored in the application delegate's backgroundSessionCompletionHandler
property. In this method, we ask the background session for all its running tasks. If there are no tasks running, we get a reference to the application delegate's background completion handler and, if it isn't nil
, we invoke it and set it to nil
.01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | - ( void )invokeBackgroundSessionCompletionHandler { [ self .session getTasksWithCompletionHandler :^( NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { NSUInteger count = [dataTasks count ] + [uploadTasks count ] + [downloadTasks count ]; if (!count) { MTAppDelegate *applicationDelegate = ( MTAppDelegate *)[[ UIApplication sharedApplication ] delegate ]; void (^backgroundSessionCompletionHandler)() = [applicationDelegate backgroundSessionCompletionHandler ]; if (backgroundSessionCompletionHandler) { [applicationDelegate setBackgroundSessionCompletionHandler :nil ]; backgroundSessionCompletionHandler(); } } }]; } |
invokeBackgroundSessionCompletionHandler
? We do this every time a download task finishes. In other words, we invoke this method in URLSession:downloadTask:didFinishDownloadingToURL:
as shown below.01 02 03 04 05 06 07 08 09 10 11 | - ( void )URLSession:( NSURLSession *)session downloadTask :( NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL :( NSURL *)location { // Write File to Disk [ self moveFileWithURL :location downloadTask :downloadTask]; // Update Progress Buffer NSURL * URL = [[downloadTask originalRequest ] URL ]; [ self .progressBuffer setObject :@( 1 .0 ) forKey :[ URL absoluteString ]]; // Invoke Background Completion Handler [ self invokeBackgroundSessionCompletionHandler ]; } |
6. Wrapping Up
I hope you agree that our podcast client isn't ready for the App Store just yet since one of the key features, playing podcasts, is still missing. As I mentioned in the previous tutorial, the focus of this project wasn't creating a full-featured podcast client. The goal of this project was illustrating how to leverage theNSURLSession
API to search the iTunes Search API and download podcast episodes using data and out-of-process download tasks respectively. You should now have a basic understanding of the NSURLSession
API as well as out-of-process tasks.Conclusion
By creating a simple podcast client, we have taken a close look at data and download tasks. We've also learned how easy it is to schedule download tasks in the background. TheNSURLSession
API is an important step forward for both iOS and OS X, and I encourage you to take advantage of this easy to use and flexible suite of classes. In the final installment of this series, I will take a look at AFNetworking 2.0. Why is it a milestone release? When should you use it? And how does it compare to the NSURLSession
API?
Comments
Post a Comment