Sunday 21 December 2014

Working with NSURLSession: Part 4

In the previous tutorial, we started creating a simple podcast client to put what we've learned about 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.

In this fourth and final tutorial about NSURLSession, 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.
Adding support for background uploads and downloads is surprisingly easy with NSURLSession. 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.
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.
At the moment, we are using prototype cells to populate the table view. To give us a bit more flexibility, we need to create a UITableViewCell subclass. Open the main storyboard, select the table view of the MTViewController instance and set the number of prototype cells to 0.
Update the project's main storyboard.
Open Xcode's File menu and choose New > File.... Create a new Objective-C class, name it MTEpisodeCell, and make sure it inherits from UITableViewCell. Tell Xcode where you'd like to store the class files and hit Create.
Create a subclass of UITableViewCell.
The interface of MTEpisodeCell 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
The implementation of MTEpisodeCell 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
We override the class's designated initializer as shown below. Note how we ignore the 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;
}
Before we take a look at the implementation of 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];
    }
}
In 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;
}
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
In the view controller's 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];
}
In 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];
}
Before we build the project and run the application, we need to update our implementation of 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;
}
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 of MTEpisodeCell.
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 the NSURLSession API. There a few gotchas though.
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
In 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];
}
Let's take a look at the implementation of 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.
It's time to make use of the background session we created and put the MTEpisodeCell 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];
}
In 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];
    }
}
As you may have guessed, 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;
}
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 NSURLSessionDelegate and NSURLSessionDownloadDelegate protocols yet. We also need to implement these methods if we want to show the progress of the download tasks.
The first method we need to implement is URLSession: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:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}
More interesting is the implementation of 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:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_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];
    });
}
The implementation of 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;
}
The third delegate method of the 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];
}
In 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];
}
In 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;
}
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.
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.
Let's start by declaring a new private property progressBuffer 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
In viewDidLoad, 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];
}
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 the tableView: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;
}
We can also use the progress buffer to prevent users from downloading the same episode twice. Take a look at the updated implementation of tableView: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];
    }
}
The progress buffer only works in its current implementation if we keep it up to date. This means that we need to update the URLSession: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:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_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];
    });
}
In 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]];
    }
}
The session delegate is notified when a download task finishes. In 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]];
}
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. The feedParser: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]];
    }
}
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.
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?
We first need to declare a property on the MTAppDelegate 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
In application: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];
}
In the MTViewController 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
We then implement another helper method, 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();
            }
        }
    }];
}
Wait a minute. When do we invoke 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];
}
Advertisement
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 the NSURLSession 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.
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. The NSURLSession 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?

No comments:

Post a Comment